Author SHA1 Message Date
KeatonTheBot 2e6e4076bf Ryujinx.slnx: Correct \ to / in RenderDocApi project path 2026-09-13 14:41:16 -05:00
KeatonTheBot ab7176e6fc Migrate RenderDoc to Kenji-NX 2026-09-13 14:27:26 -05:00
GreemDev 20868546b0 RenderDoc API support 2026-09-13 14:16:34 -05:00
Max 892fcd9444 Fixed rumble not being sent to the controller 2026-09-10 23:16:31 -05:00
KeatonTheBot 70cc617e31 Update NuGet packages
- Avalonia to 12.1.2

- Svg.Controls.Avalonia to 12.0.0.17

- Microsoft.IdentityModel.JsonWebTokens to 8.22.0

- Microsoft.NET.Test.Sdk to 18.10.0

- Ryujinx.Graphics.Vulkan.MoltenVK to 1.4.3-ryujinx.1

- System.IO.Hashing to 10.0.12
2026-09-09 21:45:41 -05:00
KeatonTheBot 9bf15801fd misc: Fix config file version 2026-09-08 16:13:26 -05:00
Max e3150d20bb [HID] Restructure HD Rumble class for future controller support
- Attempted fixing the strength: so far it hasn't been successful.
- Rumble should skip vibrations if they're not in-line with poll-rate: would like to come back to this. Queuing just does exactly what the hid buffer does, but our timer (poll rate) is not in sync with the rate the controller is reading at, which causes excess drops.
- Refactored the class so that implementing support for HD rumble for other controllers (DS5, Steam Controller) is much easier in the future.
2026-09-08 15:54:23 -05:00
Max 50538d601f [HID] Fixed HD Rumble latency 2026-09-08 15:54:23 -05:00
stossy11 5d3e392082 Fix fragment ClipDistance SPIR-V lookup
- Fixed ClipDistance being treated as VTG gl_PerVertex block, Causing the lookup of Input.Position when it's undeclared.
2026-09-08 15:54:23 -05:00
Neo c57e91cc59 Implement: TrySelectUserWithoutInteraction Command 52
Adds support for IAccountServiceForApplication command 52, TrySelectUserWithoutInteraction, introduced in Firmware 19.0.0.

Command 51 (TrySelectUserWithoutInteractionDeprecated) and command 52 use the existing ApplicationServiceServer.TrySelectUserWithoutInteraction implementation.

This prevents newer titles from failing when requesting automatic user selection through the new command.

Tomb Raider: Definitive Edition is now fully playable.
2026-09-08 15:54:22 -05:00
Neo 0381c7157b Fix: GetAllAvailableResources Filtering by Resource Path
Fix EmbeddedResources.GetAllAvailableResources to respect the specified resource path when enumerating embedded resources.

Previously, resources were filtered only by file extension. Adding locale-named JSON files under anywhere else caused them to be included when querying Assets/Locales, resulting in duplicate entries in the language menu.
2026-09-08 15:54:22 -05:00
avan 93b4c53c8a Fix crash on duplicate Build IDs in ModLoader.LoadCheats
Multiple executables may report the same Build ID, causing ToDictionary to throw an ArgumentException while creating the executable lookup.

Build the lookup incrementally and keep the first code address when duplicate Build IDs are encountered. Log a warning if a duplicate Build ID is associated with a different code address.
2026-09-08 15:54:22 -05:00
avan ef14467d1f Fix a hang during the loading stage
In KAddressArbiter, threads were originally inserted into the wait queue according to their DynamicPriority.

When multiple threads had the same DynamicPriority, the original implementation could not guarantee their existing order. Threads with the same dynamic priority are now inserted in FIFO order, preventing unstable wake-up ordering from blocking the guest synchronization flow.
2026-09-08 15:54:22 -05:00
KeatonTheBot e07f333a31 Raise application pool sizes for DRAM selections
- Fixes crash with BotW and possibly other games
2026-09-05 14:09:46 -05:00
KeatonTheBot fc4c45ef1d UI: Fix XCI file trimmer selection crash 2026-08-29 17:33:11 -05:00
Neo 52c6a1890e Improve "Add raw copy dependencies for incompatible textures"
Initial PR was made by Avan for Ryubing.

This allows for Trails in the Sky 1st Chapter (and by extension Trails through Daybreak, Trails through Daybreak II, Trails Beyond the Horizon, and the future Trails in the Sky 2nd Chapter – tested via demo – to be fully playable.

Initial PR caused issues on macOS devices, specifically crashing in certain games (such as Mario Kart 8 Deluxe and the aforementioned Trails games). It was reverted on the original Ryubing project (at the time of writing this description) for causing small rendering issues on The Legend of Zelda: Breath of the Wild.

The rendering issues do not appear on macOS.

This commit improves the original PR by eliminating said macOS crash.

Trails in the Sky 1st writes its exposure/brightness value through an R32G32Float 1x1 texture and later reads the raw bits back through an R32Uint 2x1 texture mapped to the same guest memory. The two texture formats/dimensions are fully incompatible as texture views, so they end
up as separate host textures, and nothing kept their contents coherent - the reader saw stale zeroed data, making the exposure calculation (and the rendered image) too dark. (This is the initial fix for the game)

TextureGroup already had a raw-copy dependency mechanism intended to
handle this class of alias (TextureDependency, TextureGroupHandle
raw-copy plumbing), gated by CanCreateRawCopyDependency's strict same-guest-memory checks. This commit fixes three bugs that kept that mechanism from working correctly (and thus crashing Trails and other games, such as Mario Kart 8 Deluxe):

(1) TextureGroup.InitializeOverlaps() and TextureGroup.RegisterIncompatibleOverlap() only forwarded overlaps to CreateCopyDependency() when compatibility was LayoutIncompatible or better, silently excluding the fully Incompatible case the raw-copy path exists for. Widened both guards to let Incompatible overlaps through so CanCreateRawCopyDependency actually gets a chance to run.

(2) Once Incompatible overlaps were allowed through, CreateCopyDependency(TextureGroup, ...) could still fall back to a regular, non-raw textureCopy for such pairs, since ViewLayoutCompatible/CopySizeMatches only check byte size and were never meant to reason about fully incompatible pairs (e.g. a depth format aliasing a color format with the same byte size). On Vulkan running through MoltenVK on macOS, that non-raw copy path requires a pixel-format-reinterpreting texture view, and MoltenVK/Metal refuses to create any view onto a depth-format texture ("not castable"), aborting emulation. textureCopy is now forced false for Incompatible pairs, leaving raw copy (which already excludes depth/stencil formats) as the only route for that severity level.

(3) TextureGroupHandle.Inherit() copied a handle's pending DeferredCopy to the new handle when a view was recreated, but did not copy DeferredCopyRaw alongside it. A handle that inherited a pending raw copy would then execute it through the regular (non-raw) CopyTo path once acknowledged, reinterpreting the source bytes with the wrong row layout and corrupting the image - visible as vertical flickering stripes whenever a view happened to be recreated with a raw copy still pending. DeferredCopyRaw is now carried over together with DeferredCopy.

(As mentioned earlier, vertical stripes aren't present on macOS, so (3) doesn't negatively have an effect on macOS in any way).
2026-08-29 10:00:58 -05:00
MaxandMythrax 2288084b53 HLE: Implemented ILockAccessor and ICommonStateGetter commands and stubs for Virtual Boy – Nintendo Classics #3
- Moves emulation past the initial frame by providing the necessary inputs the game expects. Apparently games like to spin until they get what they want.

Co-authored-by: Mythrax <mythrax@mythrax-rs.org>
2026-08-29 09:49:39 -05:00
Babib3l dd519a32b6 Hotfix for the PTC version, it's adjacent comment and PTC writer logging 2026-08-29 09:49:39 -05:00
Babib3l 7be8829b0a Wire "Start games in fullscreen" option to use the new fullscreen behaviour
Final (hopefully) fix for https://github.com/Ryubing/Issues/issues/415
2026-08-25 14:51:02 -05:00
Renovate Bot 6225e142fc Update Svg.Controls to 12.0.0.15 (#22)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [Svg.Controls.Avalonia](https://github.com/wieslawsoltes/Svg.Skia) | `12.0.0.13` → `12.0.0.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Svg.Controls.Avalonia/12.0.0.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Svg.Controls.Avalonia/12.0.0.13/12.0.0.15?slim=true) |
| [Svg.Controls.Skia.Avalonia](https://github.com/wieslawsoltes/Svg.Skia) | `12.0.0.13` → `12.0.0.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Svg.Controls.Skia.Avalonia/12.0.0.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Svg.Controls.Skia.Avalonia/12.0.0.13/12.0.0.15?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNDkuNSIsInVwZGF0ZWRJblZlciI6IjQzLjI0OS41IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Kenji-NX/pulls/22
2026-08-25 04:17:37 +00:00
Renovate Bot 9e3c03a606 Update dependency FluentAvaloniaUI to 3.1.0 (#21)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [FluentAvaloniaUI](https://github.com/amwx/FluentAvalonia) | `3.0.2` → `3.1.0` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/FluentAvaloniaUI/3.1.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/FluentAvaloniaUI/3.0.2/3.1.0?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNDkuNSIsInVwZGF0ZWRJblZlciI6IjQzLjI0OS41IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Kenji-NX/pulls/21
2026-08-25 02:46:07 +00:00
KeatonTheBot 8e9bcb2a8e Update System.IO.Hashing to 10.0.11 2026-08-24 15:32:05 -05:00
Babib3l 7101f52c01 River 2 : HLE: Use per-program ownership for PTC disk caches
This PR threads process/program identity into PTC disk cache initialization so cache ownership is selected from the launched process rather than global/shared application state.

Previously, the PTC initialization path only propagated loose title/version information into the CPU layer. That was mostly fine for a single launched application, but will introduce many problems once multiple programs can be launched during the same session, given later processes could inherit cache identity from the first loaded application.
To address this issue, this PR introduces a `PtcCacheInfo` payload and applies it through the process context, CPU context, translator, and PTC initialization paths. Cache ownership is now resolved once the kernel process PID is known and includes:

- PID
- Program ID / Title ID
- Application ID
- Program index
- Display version
- Process kind
- Cache selector

`PtcCacheInfo` now owns its default title/application/version values, and `Ptc` uses that cache info directly instead of mirroring title/version fields internally.

The persistent cache key itself does remains title/version/selector based rather than PID based, so caches remain reusable across launches while still being selected from the correct process context. PID is only used for diagnostics and ownership tracing in logs.

Additional PTC logging has also been added to report cache ownership and selected paths during PTC initialization, Profiling info load/save and translation cache load/save

Both the PTC and profiler internal versions were bumped (a bunch of times lol).
2026-08-24 13:45:08 -05:00
avan e80fc00462 Fix view compatibility for compressed textures with different logical sizes
It has been confirmed that Divinity: Original Sin 2 may describe the same or overlapping guest GPU memory using BCn-compressed textures with different logical dimensions, such as 104×104 and 102×102.

BCn formats store texture data in blocks, with each BC block covering 4×4 texels. After rounding the dimensions up to complete blocks, both 104×104 and 102×102 textures require a 26×26 grid of BC blocks. As a result, they have the same block footprint at the base mip level. Ryujinx originally determined size compatibility in TextureCompatibility.ViewSizeMatches() primarily from this block footprint, which could cause these textures to be classified as Full view-compatible and allowed to share the same Vulkan VkImage directly.

However, because the two textures have different logical dimensions, their mip chains are also different. For example, at mip level 3, a 104×104 texture is reduced to 13×13, while a 102×102 texture is reduced to 12×12. If a 104×104 child texture view shares the backing image of a 102×102 parent texture, a subsequent full mip upload may attempt to write 13×13 compressed data into a Vulkan image subresource whose actual dimensions are only 12×12.

This produces a compressed buffer-to-image copy that does not match the geometry of the backing image. After the invalid command is submitted to the GPU, the Vulkan driver may asynchronously report VK_ERROR_DEVICE_LOST during command buffer submission, waiting, or presentation. In Ryujinx, this eventually appears as: VulkanException: Unexpected API error "ErrorDeviceLost".

The fix adds a logical-dimension check to TextureCompatibility.ViewSizeMatches(). If either the parent or child uses a compressed texture format, and the actual width or height of the corresponding parent mip does not match the logical width or height of the child, the relationship is no longer classified as Full view-compatible. Instead, it is downgraded to CopyOnly.

CopyOnly indicates that the two textures may still describe the same or overlapping guest memory and that their contents must remain synchronized, but they cannot directly share a single fixed-size Vulkan image. Ryujinx instead allows each texture to retain a host texture matching its own logical dimensions and synchronizes their contents through the existing copy-dependency mechanism.

As a result, the 13×13 mip of the 104×104 texture is uploaded to an actual 13×13 destination mip, while the 12×12 mip of the 102×102 texture is uploaded to an actual 12×12 destination mip.
2026-08-22 18:51:41 -05:00
avan 33cbd29c23 Fix bindless elimination failures observed in OCTOPATH TRAVELER 0:
Failed to find handle source for bindless access of type "textureBuffer".
2026-08-22 18:51:41 -05:00
avan cfc7c6039f Fix OpenGL program relaunch
The Enhanced and Classic versions of FFT are different program indices within the same application. When the Classic version is launched, the game uses ExecuteProgram to stop the current program, trigger DisposeGpu, create a new renderer and OpenGL context, and then launch the requested program.

Previously, when AppHost executed DisposeGpu, it attempted to bind the old OpenGL context and then called Device.DisposeGpu to destroy GPU resources. During the program relaunch flow, however, the old RendererHost may already have been removed from the visual tree, causing its native window to become detached.

If the old OpenGL context can no longer be bound, a ContextException is thrown, preventing the subsequent AppExit and program relaunch flow from continuing.

The updated implementation handles this case by detecting a ContextException while binding the OpenGL context when ShouldRestart is true. It then skips the destruction of GPU resources that depend on the old OpenGL context, allowing the old context to be released together with the old window. The AppExit and program relaunch flow can then continue without being interrupted.
2026-08-22 18:51:41 -05:00
avan ac6db0fe76 Fix Vulkan/OpenGL attachmentless rendering
Some fragment passes used by FFT do not have any color or depth/stencil attachments. Instead, the fragment shader writes the results directly to storage images (on OpenGL: through imageStore).

### Vulkan
When SetImage is called and FramebufferParams has no attachments, its virtual size must be updated using the width, height, and layer count of the fragment storage image. Otherwise, the attachmentless framebuffer may retain an incorrect 1x1 extent, causing backgrounds, logos, UI elements, and other rendered content to be missing.

If the first draw occurs before the storage-image descriptor is rebound, SetImage cannot yet provide the correct storage-image dimensions. Therefore, when RecreateGraphicsPipelineIfNeeded finds that FramebufferParams has no attachments and still uses the default 1x1 extent, it initializes the framebuffer dimensions from the active viewport. Otherwise, the first Vulkan draw may be restricted to a 1x1 area and render incorrectly.

The storage-image extent is therefore used as the authoritative size for FramebufferParams, while the active viewport is used as a fallback when the storage-image extent is not yet available before the first draw.

### OpenGL
A storage image is not a framebuffer attachment, so OpenGL cannot derive the framebuffer width and height from it. When the framebuffer has no attachments but a viewport with valid dimensions has already been defined, Pipeline.PreDraw must set the default framebuffer width and height from the active viewport.

Without non-zero default width and height values, the attachmentless framebuffer remains incomplete. Drawing with that framebuffer results in InvalidFramebufferOperation, so rasterization and fragment shader execution do not occur even when the storage images are bound correctly.
2026-08-22 18:51:41 -05:00
KeatonTheBot bed294bb92 UI: Fix Restart Emulation menu item not being clickable 2026-08-21 12:31:39 -05:00
Babib3l ed70f7921e feature: Add GpuDriverVersion to GPU info logging
- The Vulkan backend now reports the driver name and version using VK_KHR_driver_properties when available, with fallbacks for raw Vulkan driverVersion parsing. Intel Windows drivers also get the full package-style version from the driver UUID when exposed by the driver.
  OpenGL now carries its existing OpenGL version string through HardwareInfo.GpuDriverVersion.
2026-08-21 12:05:42 -05:00
86 changed files with 2454 additions and 349 deletions
+11 -11
View File
@@ -3,25 +3,25 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia" Version="12.1.1" /> <PackageVersion Include="Avalonia" Version="12.1.2" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" /> <PackageVersion Include="Avalonia.Desktop" Version="12.1.2" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="12.1.1" /> <PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="12.1.2" />
<PackageVersion Include="Svg.Controls.Avalonia" Version="12.0.0.13" /> <PackageVersion Include="Svg.Controls.Avalonia" Version="12.0.0.17" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="12.0.0.13" /> <PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="12.0.0.17" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Concentus" Version="2.2.2" /> <PackageVersion Include="Concentus" Version="2.2.2" />
<PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" /> <PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageVersion Include="DynamicData" Version="9.4.33" /> <PackageVersion Include="DynamicData" Version="9.4.33" />
<PackageVersion Include="FluentAvaloniaUI" Version="3.0.2" /> <PackageVersion Include="FluentAvaloniaUI" Version="3.1.0" />
<PackageVersion Include="Gommon" Version="2.8.1.2" /> <PackageVersion Include="Gommon" Version="2.8.1.2" />
<PackageVersion Include="Humanizer" Version="3.0.10" /> <PackageVersion Include="Humanizer" Version="3.0.10" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.21.0" /> <PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.22.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" /> <PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" /> <PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
<PackageVersion Include="NetCoreServer" Version="8.0.7" /> <PackageVersion Include="NetCoreServer" Version="8.0.7" />
@@ -36,7 +36,7 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Linux" Version="6.1.4-build6" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Linux" Version="6.1.4-build6" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.macOS" Version="5.0.3-build14" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.macOS" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Windows" Version="6.1.4-build6" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.Windows" Version="6.1.4-build6" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.2-ryujinx.6" /> <PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.3-ryujinx.1" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" /> <PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" />
<PackageVersion Include="Ryujinx.SDL3-CS" Version="2026.707.0" /> <PackageVersion Include="Ryujinx.SDL3-CS" Version="2026.707.0" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" /> <PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
@@ -48,7 +48,7 @@
<PackageVersion Include="SkiaSharp" Version="3.119.4" /> <PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="SPB" Version="0.0.4-build32" /> <PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.10" /> <PackageVersion Include="System.IO.Hashing" Version="10.0.12" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.0" /> <PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+1
View File
@@ -21,6 +21,7 @@
<Project Path="src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj" /> <Project Path="src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj" />
<Project Path="src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj" /> <Project Path="src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj" />
<Project Path="src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" /> <Project Path="src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" />
<Project Path="src/Ryujinx.Graphics.RenderDocApi/Ryujinx.Graphics.RenderDocApi.csproj" />
<Project Path="src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj" /> <Project Path="src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj" />
<Project Path="src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj" /> <Project Path="src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj" />
<Project Path="src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj" /> <Project Path="src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj" />
+29 -21
View File
@@ -32,14 +32,11 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 7020; //! To be incremented manually for each change to the ARMeilleure project. private const uint InternalVersion = 7031; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0"; private const string ActualDir = "0";
private const string BackupDir = "1"; private const string BackupDir = "1";
private const string TitleIdTextDefault = "0000000000000000";
private const string DisplayVersionDefault = "0";
public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1); public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1);
public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2); public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2);
public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3); public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3);
@@ -67,8 +64,7 @@ namespace ARMeilleure.Translation.PTC
private bool _disposed; private bool _disposed;
public string TitleIdText { get; private set; } public PtcCacheInfo CacheInfo { get; private set; }
public string DisplayVersion { get; private set; }
private MemoryManagerType _memoryMode; private MemoryManagerType _memoryMode;
@@ -97,8 +93,7 @@ namespace ARMeilleure.Translation.PTC
_disposed = false; _disposed = false;
TitleIdText = TitleIdTextDefault; CacheInfo = new PtcCacheInfo(0, null, null, 0, null, "Unknown", "default");
DisplayVersion = DisplayVersionDefault;
CachePathActual = string.Empty; CachePathActual = string.Empty;
CachePathBackup = string.Empty; CachePathBackup = string.Empty;
@@ -106,20 +101,24 @@ namespace ARMeilleure.Translation.PTC
Disable(); Disable();
} }
public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode, string cacheSelector) public void Initialize(PtcCacheInfo cacheInfo, bool enabled, MemoryManagerType memoryMode)
{ {
Wait(); Wait();
Profiler.Wait(); Profiler.Wait();
Profiler.ClearEntries(); Profiler.ClearEntries();
Logger.Info?.Print(LogClass.Ptc, $"Initializing Profiled Persistent Translation Cache v{InternalVersion}\n\t\t (title: {titleIdText}, version: '{displayVersion}', selector: '{cacheSelector}', enabled: {enabled})."); CacheInfo = cacheInfo;
if (!enabled || string.IsNullOrEmpty(titleIdText) || titleIdText == TitleIdTextDefault) Logger.Info?.Print(
LogClass.Ptc,
$"Initializing Profiled Persistent Translation Cache v{InternalVersion}\n\t\t " +
$"(pid: {cacheInfo.ProcessId}, title: {cacheInfo.TitleIdText}, application: {cacheInfo.ApplicationIdText}, " +
$"programIndex: {cacheInfo.ProgramIndex}, version: '{cacheInfo.DisplayVersion}', kind: {cacheInfo.ProcessKind}, " +
$"selector: '{cacheInfo.CacheSelector}', key: '{cacheInfo.CacheKey}', enabled: {enabled}).");
if (!enabled || cacheInfo.TitleIdText == PtcCacheInfo.TitleIdTextDefault)
{ {
TitleIdText = TitleIdTextDefault;
DisplayVersion = DisplayVersionDefault;
CachePathActual = string.Empty; CachePathActual = string.Empty;
CachePathBackup = string.Empty; CachePathBackup = string.Empty;
@@ -128,12 +127,10 @@ namespace ARMeilleure.Translation.PTC
return; return;
} }
TitleIdText = titleIdText;
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
_memoryMode = memoryMode; _memoryMode = memoryMode;
string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir); string workPathActual = Path.Combine(AppDataManager.GamesDirPath, CacheInfo.TitleIdText, "cache", "cpu", ActualDir);
string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir); string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, CacheInfo.TitleIdText, "cache", "cpu", BackupDir);
if (!Directory.Exists(workPathActual)) if (!Directory.Exists(workPathActual))
{ {
@@ -145,8 +142,14 @@ namespace ARMeilleure.Translation.PTC
Directory.CreateDirectory(workPathBackup); Directory.CreateDirectory(workPathBackup);
} }
CachePathActual = Path.Combine(workPathActual, DisplayVersion) + "-" + cacheSelector; CachePathActual = Path.Combine(workPathActual, CacheInfo.DisplayVersion) + "-" + CacheInfo.CacheSelector;
CachePathBackup = Path.Combine(workPathBackup, DisplayVersion) + "-" + cacheSelector; CachePathBackup = Path.Combine(workPathBackup, CacheInfo.DisplayVersion) + "-" + CacheInfo.CacheSelector;
Logger.Info?.Print(
LogClass.Ptc,
$"PPTC cache owner selected (pid: {CacheInfo.ProcessId}, title: {CacheInfo.TitleIdText}, application: {CacheInfo.ApplicationIdText}, " +
$"version: '{CacheInfo.DisplayVersion}', kind: {CacheInfo.ProcessKind}, selector: '{CacheInfo.CacheSelector}', " +
$"key: '{CacheInfo.CacheKey}', path: '{CachePathActual}').");
PreLoad(); PreLoad();
Profiler.PreLoad(); Profiler.PreLoad();
@@ -370,7 +373,12 @@ namespace ARMeilleure.Translation.PTC
long fileSize = new FileInfo(fileName).Length; long fileSize = new FileInfo(fileName).Length;
Logger.Info?.Print(LogClass.Ptc, $"{(isBackup ? "Loaded Backup Translation Cache" : "Loaded Translation Cache")} (size: {fileSize} bytes, translated functions: {GetEntriesCount()})."); Logger.Info?.Print(
LogClass.Ptc,
$"{(isBackup ? "Loaded Backup Translation Cache" : "Loaded Translation Cache")} " +
$"(pid: {CacheInfo.ProcessId}, title: {CacheInfo.TitleIdText}, version: '{CacheInfo.DisplayVersion}', kind: {CacheInfo.ProcessKind}, " +
$"selector: '{CacheInfo.CacheSelector}', key: '{CacheInfo.CacheKey}', path: '{fileName}', " +
$"size: {fileSize} bytes, translated functions: {GetEntriesCount()}).");
return true; return true;
} }
@@ -0,0 +1,37 @@
namespace ARMeilleure.Translation.PTC
{
public readonly struct PtcCacheInfo
{
public const string TitleIdTextDefault = "0000000000000000";
public const string ApplicationIdTextDefault = "0000000000000000";
public const string DisplayVersionDefault = "0";
public ulong ProcessId { get; }
public string TitleIdText { get; }
public string ApplicationIdText { get; }
public byte ProgramIndex { get; }
public string DisplayVersion { get; }
public string ProcessKind { get; }
public string CacheSelector { get; }
public string CacheKey => $"{DisplayVersion}-{CacheSelector}";
public PtcCacheInfo(
ulong processId,
string titleIdText,
string applicationIdText,
byte programIndex,
string displayVersion,
string processKind,
string cacheSelector)
{
ProcessId = processId;
TitleIdText = !string.IsNullOrEmpty(titleIdText) ? titleIdText : TitleIdTextDefault;
ApplicationIdText = !string.IsNullOrEmpty(applicationIdText) ? applicationIdText : ApplicationIdTextDefault;
ProgramIndex = programIndex;
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
ProcessKind = processKind ?? string.Empty;
CacheSelector = string.IsNullOrEmpty(cacheSelector) ? "default" : cacheSelector;
}
}
}
+12 -3
View File
@@ -23,7 +23,7 @@ namespace ARMeilleure.Translation.PTC
{ {
private const string OuterHeaderMagicString = "Pohd\0\0\0\0"; private const string OuterHeaderMagicString = "Pohd\0\0\0\0";
private const uint InternalVersion = 6698; //! Not to be incremented manually for each change to the ARMeilleure project. private const uint InternalVersion = 7031; //! Not to be incremented manually for each change to the ARMeilleure project.
private static readonly uint[] _migrateInternalVersions = private static readonly uint[] _migrateInternalVersions =
[ [
@@ -254,7 +254,12 @@ namespace ARMeilleure.Translation.PTC
long fileSize = new FileInfo(fileName).Length; long fileSize = new FileInfo(fileName).Length;
Logger.Info?.Print(LogClass.Ptc, $"{(isBackup ? "Loaded Backup Profiling Info" : "Loaded Profiling Info")} (size: {fileSize} bytes, profiled functions: {ProfiledFuncs.Count})."); Logger.Info?.Print(
LogClass.Ptc,
$"{(isBackup ? "Loaded Backup Profiling Info" : "Loaded Profiling Info")} " +
$"(pid: {_ptc.CacheInfo.ProcessId}, title: {_ptc.CacheInfo.TitleIdText}, version: '{_ptc.CacheInfo.DisplayVersion}', " +
$"kind: {_ptc.CacheInfo.ProcessKind}, selector: '{_ptc.CacheInfo.CacheSelector}', key: '{_ptc.CacheInfo.CacheKey}', " +
$"path: '{fileName}', size: {fileSize} bytes, profiled functions: {ProfiledFuncs.Count}).");
return true; return true;
} }
@@ -375,7 +380,11 @@ namespace ARMeilleure.Translation.PTC
if (fileSize != 0L) if (fileSize != 0L)
{ {
Logger.Info?.Print(LogClass.Ptc, $"Saved Profiling Info (size: {fileSize} bytes, profiled functions: {profiledFuncsCount})."); Logger.Info?.Print(
LogClass.Ptc,
$"Saved Profiling Info (pid: {_ptc.CacheInfo.ProcessId}, title: {_ptc.CacheInfo.TitleIdText}, version: '{_ptc.CacheInfo.DisplayVersion}', " +
$"kind: {_ptc.CacheInfo.ProcessKind}, selector: '{_ptc.CacheInfo.CacheSelector}', key: '{_ptc.CacheInfo.CacheKey}', " +
$"path: '{fileName}', size: {fileSize} bytes, profiled functions: {profiledFuncsCount}).");
} }
} }
+2 -2
View File
@@ -61,9 +61,9 @@ namespace ARMeilleure.Translation
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub; FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
} }
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) public IPtcLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{ {
_ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type, cacheSelector); _ptc.Initialize(cacheInfo, enabled, Memory.Type);
return _ptc; return _ptc;
} }
@@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Enable Rumble /// Enable Rumble
/// </summary> /// </summary>
public bool EnableRumble { get; set; } public bool EnableRumble { get; set; }
/// <summary>
/// Enable HD Rumble support
/// </summary
public bool UseHDRumble { get; set; }
} }
} }
@@ -126,8 +126,13 @@ namespace Ryujinx.Common
public static string[] GetAllAvailableResources(string path, string ext = "") public static string[] GetAllAvailableResources(string path, string ext = "")
{ {
return ResolveManifestPath(path).Item1.GetManifestResourceNames() (Assembly assembly, string resourcePath) = ResolveManifestPath(path);
.Where(r => r.EndsWith(ext))
string manifestPath = assembly.GetName().Name + "." + resourcePath.Replace('/', '.');
return assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(manifestPath + ".", StringComparison.Ordinal))
.Where(r => r.EndsWith(ext, StringComparison.Ordinal))
.ToArray(); .ToArray();
} }
+2 -1
View File
@@ -1,4 +1,5 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Translation.PTC;
using System.Runtime.Versioning; using System.Runtime.Versioning;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
@@ -32,7 +33,7 @@ namespace Ryujinx.Cpu.AppleHv
{ {
} }
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) public IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{ {
return new DummyDiskCacheLoadState(); return new DummyDiskCacheLoadState();
} }
+3 -3
View File
@@ -1,4 +1,5 @@
using System; using System;
using ARMeilleure.Translation.PTC;
namespace Ryujinx.Cpu namespace Ryujinx.Cpu
{ {
@@ -44,11 +45,10 @@ namespace Ryujinx.Cpu
/// <remarks> /// <remarks>
/// If the execution engine is recompiling guest code, this can be used to load cached code from disk. /// If the execution engine is recompiling guest code, this can be used to load cached code from disk.
/// </remarks> /// </remarks>
/// <param name="titleIdText">Title ID of the application in padded hex form</param> /// <param name="cacheInfo">Identity and selector for the process-owned disk cache</param>
/// <param name="displayVersion">Version of the application</param>
/// <param name="enabled">True if the cache should be loaded from disk if it exists, false otherwise</param> /// <param name="enabled">True if the cache should be loaded from disk if it exists, false otherwise</param>
/// <returns>Disk cache load progress reporter and manager</returns> /// <returns>Disk cache load progress reporter and manager</returns>
IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector); IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled);
/// <summary> /// <summary>
/// Indicates that code has been loaded into guest memory, and that it might be executed in the future. /// Indicates that code has been loaded into guest memory, and that it might be executed in the future.
+3 -2
View File
@@ -1,6 +1,7 @@
using ARMeilleure.Common; using ARMeilleure.Common;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Translation; using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC;
using Ryujinx.Cpu.Signal; using Ryujinx.Cpu.Signal;
namespace Ryujinx.Cpu.Jit namespace Ryujinx.Cpu.Jit
@@ -51,9 +52,9 @@ namespace Ryujinx.Cpu.Jit
} }
/// <inheritdoc/> /// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) public IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{ {
return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled, cacheSelector)); return new JitDiskCacheLoadState(_translator.LoadDiskCache(cacheInfo, enabled));
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -1,5 +1,6 @@
using ARMeilleure.Common; using ARMeilleure.Common;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Translation.PTC;
using Ryujinx.Cpu.Jit; using Ryujinx.Cpu.Jit;
using Ryujinx.Cpu.LightningJit.State; using Ryujinx.Cpu.LightningJit.State;
@@ -46,7 +47,7 @@ namespace Ryujinx.Cpu.LightningJit
} }
/// <inheritdoc/> /// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) public IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{ {
return new DummyDiskCacheLoadState(); return new DummyDiskCacheLoadState();
} }
+3 -1
View File
@@ -5,12 +5,14 @@ namespace Ryujinx.Graphics.GAL
public string GpuVendor { get; } public string GpuVendor { get; }
public string GpuModel { get; } public string GpuModel { get; }
public string GpuDriver { get; } public string GpuDriver { get; }
public string GpuDriverVersion { get; }
public HardwareInfo(string gpuVendor, string gpuModel, string gpuDriver) public HardwareInfo(string gpuVendor, string gpuModel, string gpuDriver, string gpuDriverVersion)
{ {
GpuVendor = gpuVendor; GpuVendor = gpuVendor;
GpuModel = gpuModel; GpuModel = gpuModel;
GpuDriver = gpuDriver; GpuDriver = gpuDriver;
GpuDriverVersion = gpuDriverVersion;
} }
} }
} }
+10 -11
View File
@@ -16,19 +16,18 @@ namespace Ryujinx.Graphics.GAL
public static class TargetExtensions public static class TargetExtensions
{ {
public static bool IsMultisample(this Target target) extension(Target target)
{ {
return target is Target.Texture2DMultisample or Target.Texture2DMultisampleArray; public bool IsMultisample => target is Target.Texture2DMultisample or Target.Texture2DMultisampleArray;
}
public static bool HasDepthOrLayers(this Target target) public bool HasDepthOrLayers =>
{ target is
return target is Target.Texture3D Target.Texture3D or
or Target.Texture1DArray Target.Texture1DArray or
or Target.Texture2DArray Target.Texture2DArray or
or Target.Texture2DMultisampleArray Target.Texture2DMultisampleArray or
or Target.Cubemap Target.Cubemap or
or Target.CubemapArray; Target.CubemapArray;
} }
} }
} }
+3 -3
View File
@@ -1117,7 +1117,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <returns>True if data was flushed, false otherwise</returns> /// <returns>True if data was flushed, false otherwise</returns>
public bool FlushModified(bool tracked = true) public bool FlushModified(bool tracked = true)
{ {
return TextureCompatibility.CanTextureFlush(this, _context.Capabilities) && Group.FlushModified(this, tracked); return TextureCompatibility.CanTextureFlush(Info, _context.Capabilities) && Group.FlushModified(this, tracked);
} }
/// <summary> /// <summary>
@@ -1131,7 +1131,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="tracked">Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either.</param> /// <param name="tracked">Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either.</param>
public void Flush(bool tracked) public void Flush(bool tracked)
{ {
if (TextureCompatibility.CanTextureFlush(this, _context.Capabilities)) if (TextureCompatibility.CanTextureFlush(Info, _context.Capabilities))
{ {
FlushTextureDataToGuest(tracked); FlushTextureDataToGuest(tracked);
} }
@@ -1336,7 +1336,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info, ref caps)); result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info, ref caps));
bool bothMs = Info.Target.IsMultisample() && info.Target.IsMultisample(); bool bothMs = Info.Target.IsMultisample && info.Target.IsMultisample;
if (bothMs && (Info.SamplesInX != info.SamplesInX || Info.SamplesInY != info.SamplesInY)) if (bothMs && (Info.SamplesInX != info.SamplesInX || Info.SamplesInY != info.SamplesInY))
{ {
result = TextureViewCompatibility.Incompatible; result = TextureViewCompatibility.Incompatible;
@@ -195,16 +195,6 @@ namespace Ryujinx.Graphics.Gpu.Image
return true; return true;
} }
/// <summary>
/// Determines whether a texture can flush its data back to guest memory.
/// </summary>
/// <param name="info">Texture that will have its data flushed</param>
/// <param name="caps">Host GPU Capabilities</param>
/// <returns>True if the texture can flush, false otherwise</returns>
public static bool CanTextureFlush(Texture texture, in Capabilities caps)
{
return !texture.HasImportOverride() && CanTextureFlush(texture.Info, caps);
}
/// <summary> /// <summary>
/// Determines whether a texture can flush its data back to guest memory. /// Determines whether a texture can flush its data back to guest memory.
@@ -212,14 +202,15 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="info">Texture information</param> /// <param name="info">Texture information</param>
/// <param name="caps">Host GPU Capabilities</param> /// <param name="caps">Host GPU Capabilities</param>
/// <returns>True if the texture can flush, false otherwise</returns> /// <returns>True if the texture can flush, false otherwise</returns>
private static bool CanTextureFlush(TextureInfo info, in Capabilities caps) public static bool CanTextureFlush(TextureInfo info, Capabilities caps)
{ {
if (IsFormatHostIncompatible(info, in caps)) if (IsFormatHostIncompatible(info, caps))
{ {
return false; // Flushing this format is not supported, as it may have been converted to another host format. return false; // Flushing this format is not supported, as it may have been converted to another host format.
} }
if (info.Target is Target.Texture2DMultisample or Target.Texture2DMultisampleArray) if (info.Target is Target.Texture2DMultisample or
Target.Texture2DMultisampleArray)
{ {
return false; // Flushing multisample textures is not supported, the host does not allow getting their data. return false; // Flushing multisample textures is not supported, the host does not allow getting their data.
} }
@@ -381,12 +372,15 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
// Some APIs align the width for copy and render target textures, // Some APIs align the width for copy and render target textures,
// so the width may not match in this case for different uses of the same texture. // so the width may not match for different uses of the same texture.
// To account for this, we compare the aligned width here. // To account for this, we compare the aligned width here.
// We expect height to always match exactly, if the texture is the same. // However, matching block footprints are not sufficient for compressed textures;
// their logical dimensions must also match.
if (alignedWidthMatches && lhsSize.Height == rhsSize.Height) if (alignedWidthMatches && lhsSize.Height == rhsSize.Height)
{ {
return (exact && lhsSize.Width != rhsSize.Width) || lhsSize.Width < rhsSize.Width return ((lhs.FormatInfo.IsCompressed || rhs.FormatInfo.IsCompressed) &&
(Math.Max(1, lhs.Width >> level) != rhs.Width || Math.Max(1, lhs.Height >> level) != rhs.Height)) ||
(exact && lhsSize.Width != rhsSize.Width) || lhsSize.Width < rhsSize.Width
? TextureViewCompatibility.CopyOnly ? TextureViewCompatibility.CopyOnly
: result; : result;
} }
@@ -397,7 +391,7 @@ namespace Ryujinx.Graphics.Gpu.Image
return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.LayoutIncompatible; return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.LayoutIncompatible;
} }
else if (lhs.Target.IsMultisample() != rhs.Target.IsMultisample() && alignedWidthMatches && lhsAlignedSize.Height == rhsAlignedSize.Height) else if (lhs.Target.IsMultisample != rhs.Target.IsMultisample && alignedWidthMatches && lhsAlignedSize.Height == rhsAlignedSize.Height)
{ {
// Copy between multisample and non-multisample textures with mismatching size is allowed, // Copy between multisample and non-multisample textures with mismatching size is allowed,
// as long aligned size matches. // as long aligned size matches.
+32 -20
View File
@@ -147,7 +147,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_allOffsets = size.AllOffsets; _allOffsets = size.AllOffsets;
_sliceSizes = size.SliceSizes; _sliceSizes = size.SliceSizes;
if (Storage.Target.HasDepthOrLayers() && Storage.Info.GetSlices() > GranularLayerThreshold) if (Storage.Target.HasDepthOrLayers && Storage.Info.GetSlices() > GranularLayerThreshold)
{ {
_hasLayerViews = true; _hasLayerViews = true;
_hasMipViews = true; _hasMipViews = true;
@@ -182,7 +182,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
foreach (TextureIncompatibleOverlap overlap in _incompatibleOverlaps) foreach (TextureIncompatibleOverlap overlap in _incompatibleOverlaps)
{ {
if (overlap.Compatibility <= TextureViewCompatibility.LayoutIncompatible) // LayoutIncompatible and better may still use a regular texture copy dependency.
// Fully Incompatible pairs are not copy compatible in general, but may still qualify for an
// exact raw byte copy dependency (checked internally by CreateCopyDependency) when they map
// to exactly the same guest memory, such as differently typed/sized aliases of the same data.
if (overlap.Compatibility <= TextureViewCompatibility.Incompatible)
{ {
CreateCopyDependency(overlap.Group, false, overlap.Compatibility); CreateCopyDependency(overlap.Group, false, overlap.Compatibility);
} }
@@ -226,7 +230,6 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
} }
/// <summary> /// <summary>
/// Flushes incompatible overlaps if the storage format requires it, and they have been modified. /// Flushes incompatible overlaps if the storage format requires it, and they have been modified.
/// This allows unsupported host formats to accept data written to format aliased textures. /// This allows unsupported host formats to accept data written to format aliased textures.
@@ -324,7 +327,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
FlushIncompatibleOverlapsIfNeeded(); FlushIncompatibleOverlapsIfNeeded();
EvaluateRelevantHandles(texture, (baseHandle, regionCount, split, _) => EvaluateRelevantHandles(texture, (baseHandle, regionCount, split, bound) =>
{ {
bool dirty = false; bool dirty = false;
bool anyModified = false; bool anyModified = false;
@@ -479,7 +482,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="texture">The texture to synchronize dependents of</param> /// <param name="texture">The texture to synchronize dependents of</param>
public void SynchronizeDependents(Texture texture) public void SynchronizeDependents(Texture texture)
{ {
EvaluateRelevantHandles(texture, (baseHandle, regionCount, _, _) => EvaluateRelevantHandles(texture, (baseHandle, regionCount, split, bound) =>
{ {
for (int i = 0; i < regionCount; i++) for (int i = 0; i < regionCount; i++)
{ {
@@ -571,7 +574,7 @@ namespace Ryujinx.Graphics.Gpu.Image
tracked = tracked || ShouldFlushTriggerTracking(); tracked = tracked || ShouldFlushTriggerTracking();
bool flushed = false; bool flushed = false;
EvaluateRelevantHandles(texture, (baseHandle, regionCount, split, _) => EvaluateRelevantHandles(texture, (baseHandle, regionCount, split, bound) =>
{ {
int startSlice = 0; int startSlice = 0;
int endSlice = 0; int endSlice = 0;
@@ -652,14 +655,14 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_flushBuffer == BufferHandle.Null) if (_flushBuffer == BufferHandle.Null)
{ {
if (!TextureCompatibility.CanTextureFlush(Storage, _context.Capabilities)) if (!TextureCompatibility.CanTextureFlush(Storage.Info, _context.Capabilities))
{ {
return; return;
} }
bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel; bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel;
IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0; nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size)) if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size))
{ {
@@ -716,7 +719,7 @@ namespace Ryujinx.Graphics.Gpu.Image
ClearIncompatibleOverlaps(texture); ClearIncompatibleOverlaps(texture);
EvaluateRelevantHandles(texture, (baseHandle, regionCount, _, _) => EvaluateRelevantHandles(texture, (baseHandle, regionCount, split, bound) =>
{ {
for (int i = 0; i < regionCount; i++) for (int i = 0; i < regionCount; i++)
{ {
@@ -1049,7 +1052,7 @@ namespace Ryujinx.Graphics.Gpu.Image
int endOffset = _allOffsets[viewEnd] + _sliceSizes[lastLevel]; int endOffset = _allOffsets[viewEnd] + _sliceSizes[lastLevel];
int size = endOffset - offset; int size = endOffset - offset;
List<RegionHandle> result = new(); List<RegionHandle> result = [];
for (int i = 0; i < TextureRange.Count; i++) for (int i = 0; i < TextureRange.Count; i++)
{ {
@@ -1163,7 +1166,6 @@ namespace Ryujinx.Graphics.Gpu.Image
SignalAllDirty(); SignalAllDirty();
} }
/// <summary> /// <summary>
/// Removes a view from the group, removing it from all overlap lists. /// Removes a view from the group, removing it from all overlap lists.
/// </summary> /// </summary>
@@ -1385,7 +1387,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_is3D) if (_is3D)
{ {
List<TextureGroupHandle> handlesList = new(); List<TextureGroupHandle> handlesList = [];
for (int i = 0; i < levelHandles; i++) for (int i = 0; i < levelHandles; i++)
{ {
@@ -1468,15 +1470,15 @@ namespace Ryujinx.Graphics.Gpu.Image
// Get the location of each texture within its storage, so we can find the handles to apply the dependency to. // Get the location of each texture within its storage, so we can find the handles to apply the dependency to.
// This can consist of multiple disjoint regions, for example if this is a mip slice of an array texture. // This can consist of multiple disjoint regions, for example if this is a mip slice of an array texture.
List<(int BaseHandle, int RegionCount)> targetRange = new(); List<(int BaseHandle, int RegionCount)> targetRange = [];
List<(int BaseHandle, int RegionCount)> otherRange = new(); List<(int BaseHandle, int RegionCount)> otherRange = [];
EvaluateRelevantHandles(firstLayer, firstLevel, other.Info.GetSlices(), other.Info.Levels, (baseHandle, regionCount, _, _) => EvaluateRelevantHandles(firstLayer, firstLevel, other.Info.GetSlices(), other.Info.Levels, (baseHandle, regionCount, split, specialData) =>
{ {
targetRange.Add((baseHandle, regionCount)); targetRange.Add((baseHandle, regionCount));
return true; return true;
}, out _); }, out _);
otherGroup.EvaluateRelevantHandles(other, (baseHandle, regionCount, _, _) => otherGroup.EvaluateRelevantHandles(other, (baseHandle, regionCount, split, specialData) =>
{ {
otherRange.Add((baseHandle, regionCount)); otherRange.Add((baseHandle, regionCount));
return true; return true;
@@ -1601,7 +1603,15 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureInfo info = Storage.Info; TextureInfo info = Storage.Info;
TextureInfo otherInfo = other.Storage.Info; TextureInfo otherInfo = other.Storage.Info;
bool textureCopy = TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) && // ViewLayoutCompatible/CopySizeMatches only reason about textures with some genuine
// format relationship (LayoutIncompatible or better) - they are not aware of, and must
// never be used to justify, a plain texture-to-texture copy (which some backends
// implement via a reinterpreting view) between fully Incompatible aliases such as a
// depth format and an unrelated color format. For Incompatible pairs, only the strict
// raw byte copy dependency below (which explicitly excludes depth/stencil formats) may
// be used.
bool textureCopy = compatibility != TextureViewCompatibility.Incompatible &&
TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) &&
TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel); TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel);
if (textureCopy || rawCopy) if (textureCopy || rawCopy)
@@ -1659,9 +1669,12 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
if (!_incompatibleOverlaps.Any(overlap => overlap.Group == other.Group)) if (!_incompatibleOverlaps.Any(overlap => overlap.Group == other.Group))
{ {
if (copy && other.Compatibility <= TextureViewCompatibility.LayoutIncompatible) if (copy && other.Compatibility <= TextureViewCompatibility.Incompatible)
{ {
// Any of the group's views may share compatibility, even if the parents do not fully. // Any of the group's views may share compatibility, even if the parents do not fully.
// Fully Incompatible groups are also let through here, since CreateCopyDependency will
// fall back to an exact raw byte copy dependency for them when the strict requirements
// for one are met (see CanCreateRawCopyDependency).
CreateCopyDependency(other.Group, false, other.Compatibility); CreateCopyDependency(other.Group, false, other.Compatibility);
} }
@@ -1763,7 +1776,7 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
} }
if (TextureCompatibility.CanTextureFlush(Storage, _context.Capabilities) && !(inBuffer && _flushBufferImported)) if (TextureCompatibility.CanTextureFlush(Storage.Info, _context.Capabilities) && !(inBuffer && _flushBufferImported))
{ {
FlushSliceRange(false, handle.BaseSlice, handle.BaseSlice + handle.SliceCount, inBuffer, Storage.GetFlushTexture()); FlushSliceRange(false, handle.BaseSlice, handle.BaseSlice + handle.SliceCount, inBuffer, Storage.GetFlushTexture());
} }
@@ -1803,4 +1816,3 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
} }
} }
@@ -717,6 +717,7 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
DeferredCopy = old.DeferredCopy; DeferredCopy = old.DeferredCopy;
DeferredCopyRaw = old.DeferredCopyRaw;
} }
} }
+36 -2
View File
@@ -20,6 +20,27 @@ namespace Ryujinx.Graphics.OpenGL
private int _colorsCount; private int _colorsCount;
private bool _dualSourceBlend; private bool _dualSourceBlend;
public bool HasAttachments
{
get
{
if (_depthStencil != null)
{
return true;
}
for (int index = 0; index < _colors.Length; index++)
{
if (_colors[index] != null)
{
return true;
}
}
return false;
}
}
public Framebuffer() public Framebuffer()
{ {
Handle = GL.GenFramebuffer(); Handle = GL.GenFramebuffer();
@@ -34,6 +55,12 @@ namespace Ryujinx.Graphics.OpenGL
return Handle; return Handle;
} }
public void SetDefaultSize(int width, int height)
{
GL.FramebufferParameter(FramebufferTarget.Framebuffer, FramebufferDefaultParameter.FramebufferDefaultWidth, Math.Max(1, width));
GL.FramebufferParameter(FramebufferTarget.Framebuffer, FramebufferDefaultParameter.FramebufferDefaultHeight, Math.Max(1, height));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AttachColor(int index, TextureView color) public void AttachColor(int index, TextureView color)
{ {
@@ -105,13 +132,20 @@ namespace Ryujinx.Graphics.OpenGL
_colorsCount = colorsCount; _colorsCount = colorsCount;
} }
private static void SetDrawBuffersImpl(int colorsCount) private void SetDrawBuffersImpl(int colorsCount)
{ {
DrawBuffersEnum[] drawBuffers = new DrawBuffersEnum[colorsCount]; DrawBuffersEnum[] drawBuffers = new DrawBuffersEnum[colorsCount];
for (int index = 0; index < colorsCount; index++) for (int index = 0; index < colorsCount; index++)
{ {
drawBuffers[index] = DrawBuffersEnum.ColorAttachment0 + index; if (_colors[index] != null)
{
drawBuffers[index] = DrawBuffersEnum.ColorAttachment0 + index;
}
else
{
drawBuffers[index] = DrawBuffersEnum.None;
}
} }
GL.DrawBuffers(colorsCount, drawBuffers); GL.DrawBuffers(colorsCount, drawBuffers);
@@ -116,8 +116,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
{ {
TextureView destinationView = (TextureView)destination; TextureView destinationView = (TextureView)destination;
bool srcIsMultisample = Target.IsMultisample(); bool srcIsMultisample = Target.IsMultisample;
bool dstIsMultisample = destinationView.Target.IsMultisample(); bool dstIsMultisample = destinationView.Target.IsMultisample;
if (dstIsMultisample != srcIsMultisample && Info.Format.IsDepthOrStencil()) if (dstIsMultisample != srcIsMultisample && Info.Format.IsDepthOrStencil())
{ {
@@ -172,8 +172,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
{ {
TextureView destinationView = (TextureView)destination; TextureView destinationView = (TextureView)destination;
bool srcIsMultisample = Target.IsMultisample(); bool srcIsMultisample = Target.IsMultisample;
bool dstIsMultisample = destinationView.Target.IsMultisample(); bool dstIsMultisample = destinationView.Target.IsMultisample;
if (dstIsMultisample != srcIsMultisample && Info.Format.IsDepthOrStencil()) if (dstIsMultisample != srcIsMultisample && Info.Format.IsDepthOrStencil())
{ {
@@ -216,7 +216,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
Extents2D srcRegion = new(0, 0, Width, Height); Extents2D srcRegion = new(0, 0, Width, Height);
Extents2D dstRegion = new(0, 0, destinationView.Width, destinationView.Height); Extents2D dstRegion = new(0, 0, destinationView.Width, destinationView.Height);
if (destinationView.Target.IsMultisample()) if (destinationView.Target.IsMultisample)
{ {
TextureView intermmediate = _renderer.TextureCopy.IntermediatePool.GetOrCreateWithAtLeast( TextureView intermmediate = _renderer.TextureCopy.IntermediatePool.GetOrCreateWithAtLeast(
Info.Target, Info.Target,
@@ -133,7 +133,7 @@ namespace Ryujinx.Graphics.OpenGL
public HardwareInfo GetHardwareInfo() public HardwareInfo GetHardwareInfo()
{ {
return new HardwareInfo(GpuVendor, GpuRenderer, GpuVendor); // OpenGL does not provide a driver name, vendor name is closest analogue. return new HardwareInfo(GpuVendor, GpuRenderer, GpuVendor, GpuVersion); // OpenGL does not provide a driver name, vendor name is closest analogue.
} }
public PinnedSpan<byte> GetBufferData(BufferHandle buffer, int offset, int size) public PinnedSpan<byte> GetBufferData(BufferHandle buffer, int offset, int size)
+5
View File
@@ -1537,6 +1537,11 @@ namespace Ryujinx.Graphics.OpenGL
{ {
DrawCount++; DrawCount++;
if (!_framebuffer.HasAttachments && _viewportArray.Length >= 4)
{
_framebuffer.SetDefaultSize((int)_viewportArray[2], (int)_viewportArray[3]);
}
_unit0Texture?.Bind(0); _unit0Texture?.Bind(0);
} }
@@ -0,0 +1,12 @@
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
public readonly record struct Capture(int Index, string FileName, DateTime Timestamp)
{
public void SetComments(string comments)
{
RenderDoc.SetCaptureFileComments(FileName, comments);
}
}
}
@@ -0,0 +1,100 @@
// ReSharper disable UnusedMember.Global
namespace Ryujinx.Graphics.RenderDocApi
{
public enum CaptureOption
{
/// <summary>
/// specifies whether the application is allowed to enable vsync. Default is on.
/// </summary>
AllowVsync = 0,
/// <summary>
/// specifies whether the application is allowed to enter exclusive fullscreen. Default is on.
/// </summary>
AllowFullscreen = 1,
/// <summary>
/// specifies whether (where possible) API-specific debugging is enabled. Default is off.
/// </summary>
ApiValidation = 2,
/// <summary>
/// specifies whether each API call should save a callstack. Default is off.
/// </summary>
CaptureCallstacks = 3,
/// <summary>
/// specifies whether, if <see cref="CaptureCallstacks"/> is enabled, callstacks are only saved on actions. Default is off.
/// </summary>
CaptureCallstacksOnlyDraws = 4,
/// <summary>
/// specifies a delay in seconds after launching a process to pause, to allow debuggers to attach. <br/>
/// This will only apply to child processes since the delay happens at process startup. Default is 0.
/// </summary>
DelayForDebugger = 5,
/// <summary>
/// specifies whether any mapped memory updates should be bounds-checked for overruns,
/// and uninitialised buffers are initialized to <code>0xDDDDDDDD</code> to catch use of uninitialised data.
/// Only supported on D3D11 and OpenGL. Default is off.
/// </summary>
/// <remarks>
/// This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do
/// not do the same kind of interception &amp; checking, and undefined contents are really undefined.
/// </remarks>
VerifyBufferAccess = 6,
/// <summary>
/// Hooks any system API calls that create child processes, and injects
/// RenderDoc into them recursively with the same options.
/// </summary>
HookIntoChildren = 7,
/// <summary>
/// specifies whether all live resources at the time of capture should be included in the capture,
/// even if they are not referenced by the frame. Default is off.
/// </summary>
RefAllSources = 8,
/// <summary>
/// By default, RenderDoc skips saving initial states for resources where the
/// previous contents don't appear to be used, assuming that writes before
/// reads indicate previous contents aren't used.
/// </summary>
/// <remarks>
/// **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or
/// getting it will be ignored, to allow compatibility with older versions.
/// In v1.1 the option acts as if it's always enabled.
/// </remarks>
SaveAllInitials = 9,
/// <summary>
/// In APIs that allow for the recording of command lists to be replayed later,
/// RenderDoc may choose to not capture command lists before a frame capture is
/// triggered, to reduce overheads. This means any command lists recorded once
/// and replayed many times will not be available and may cause a failure to
/// capture.
/// </summary>
/// <remarks>
/// NOTE: This is only true for APIs where multithreading is difficult or
/// discouraged. Newer APIs like Vulkan and D3D12 will ignore this option
/// and always capture all command lists since the API is heavily oriented
/// around it and the overheads have been reduced by API design.
/// </remarks>
CaptureAllCmdLists = 10,
/// <summary>
/// Mute API debugging output when the <see cref="ApiValidation"/> option is enabled.
/// </summary>
DebugOutputMute = 11,
/// <summary>
/// Allow vendor extensions to be used even when they may be
/// incompatible with RenderDoc and cause corrupted replays or crashes.
/// </summary>
AllowUnsupportedVendorExtensions = 12,
/// <summary>
/// Define a soft memory limit which some APIs may aim to keep overhead under where
/// possible. Anything above this limit will where possible be saved directly to disk during
/// capture.<br/>
/// This will cause increased disk space use (which may cause a capture to fail if disk space is
/// exhausted) as well as slower capture times.
/// <br/><br/>
/// Not all memory allocations may be deferred like this so it is not a guarantee of a memory
/// limit.
/// <br/><br/>
/// Units are in MBs, suggested values would range from 200MB to 1000MB.
/// </summary>
SoftMemoryLimit = 13,
}
}
@@ -0,0 +1,83 @@
// ReSharper disable UnusedMember.Global
namespace Ryujinx.Graphics.RenderDocApi
{
public enum InputButton
{
// '0' - '9' matches ASCII values
Key0 = 0x30,
Key1 = 0x31,
Key2 = 0x32,
Key3 = 0x33,
Key4 = 0x34,
Key5 = 0x35,
Key6 = 0x36,
Key7 = 0x37,
Key8 = 0x38,
Key9 = 0x39,
// 'A' - 'Z' matches ASCII values
A = 0x41,
B = 0x42,
C = 0x43,
D = 0x44,
E = 0x45,
F = 0x46,
G = 0x47,
H = 0x48,
I = 0x49,
J = 0x4A,
K = 0x4B,
L = 0x4C,
M = 0x4D,
N = 0x4E,
O = 0x4F,
P = 0x50,
Q = 0x51,
R = 0x52,
S = 0x53,
T = 0x54,
U = 0x55,
V = 0x56,
W = 0x57,
X = 0x58,
Y = 0x59,
Z = 0x5A,
// leave the rest of the ASCII range free
// in case we want to use it later
NonPrintable = 0x100,
Divide,
Multiply,
Subtract,
Plus,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
Home,
End,
Insert,
Delete,
PageUp,
PageDn,
Backspace,
Tab,
PrtScrn,
Pause,
Max,
}
}
@@ -0,0 +1,39 @@
// ReSharper disable UnusedMember.Global
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
[Flags]
public enum OverlayBits
{
/// <summary>
/// This single bit controls whether the overlay is enabled or disabled globally
/// </summary>
Enabled = 1 << 0,
/// <summary>
/// Show the average framerate over several seconds as well as min/max
/// </summary>
FrameRate = 1 << 1,
/// <summary>
/// Show the current frame number
/// </summary>
FrameNumber = 1 << 2,
/// <summary>
/// Show a list of recent captures, and how many captures have been made
/// </summary>
CaptureList = 1 << 3,
/// <summary>
/// Default values for the overlay mask
/// </summary>
Default = Enabled | FrameRate | FrameNumber | CaptureList,
/// <summary>
/// Enable all bits
/// </summary>
All = ~0,
/// <summary>
/// Disable all bits
/// </summary>
None = 0
}
}
@@ -0,0 +1,5 @@
# Ryujinx.Graphics.RenderDocApi
This is a C# binding for RenderDoc's application API.
This is a source-inclusion of https://github.com/utkumaden/RenderdocSharp.
I didn't use the NuGet package as I had a few minor changes I wanted to make, and I want to learn from it as well via hands-on experience.
@@ -0,0 +1,639 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Ryujinx.Graphics.RenderDocApi
{
public static unsafe partial class RenderDoc
{
/// <summary>
/// True if the API is available.
/// </summary>
public static bool IsAvailable => Api != null;
/// <summary>
/// Set the minimum version of the API you require.
/// </summary>
/// <remarks>Set this before you do anything else with the RenderDoc API, including <see cref="IsAvailable"/>.</remarks>
public static RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.Version_1_0_0;
/// <summary>
/// Set to true to assert versions.
/// </summary>
public static bool AssertVersionEnabled { get; set; } = true;
/// <summary>
/// Version of the API available.
/// </summary>
[MemberNotNullWhen(true, nameof(IsAvailable))]
public static Version? Version
{
get
{
if (!IsAvailable)
return null;
int major, minor, build;
Api->GetApiVersion(&major, &minor, &build);
return new Version(major, minor, build);
}
}
/// <summary>
/// The current mask which determines what sections of the overlay render on each window.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static OverlayBits OverlayBits
{
get => Api->GetOverlayBits();
set
{
Api->MaskOverlayBits(~value, value);
}
}
/// <summary>
/// The template for new captures.<br/>
/// The template can either be a relative or absolute path, which determines where captures will be saved and how they will be named.
/// If the path template is 'my_captures/example', then captures saved will be e.g.
/// 'my_captures/example_frame123.rdc' and 'my_captures/example_frame456.rdc'.<br/>
/// Relative paths will be saved relative to the process’s current working directory.<br/>
/// </summary>
/// <remarks>The default template is in a folder controlled by the UI - initially the system temporary folder, and the filename is the executable’s filename.</remarks>
[RenderDocApiVersion(1, 0)]
public static string CaptureFilePathTemplate
{
get
{
byte* ptr = Api->GetCaptureFilePathTemplate();
return Marshal.PtrToStringUTF8((nint)ptr)!;
}
set
{
fixed (byte* ptr = value.ToNullTerminatedByteArray())
{
Api->SetCaptureFilePathTemplate(ptr);
}
}
}
/// <summary>
/// The amount of frame captures that have been made.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static int CaptureCount => Api->GetNumCaptures();
/// <summary>
/// Checks if the RenderDoc UI is currently connected to this process.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static bool IsTargetControlConnected => Api is not null && Api->IsTargetControlConnected() != 0;
/// <summary>
/// Checks if the current frame is capturing.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static bool IsFrameCapturing => Api is not null && Api->IsFrameCapturing() != 0;
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="integer">the unsigned integer value to set for the option.</param>
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, uint integer)
{
return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0;
}
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="boolean">the value to set for the option, converted to a 0 or 1 before setting.</param>
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, bool boolean)
=> SetCaptureOption(option, boolean ? 1 : 0);
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="single">the floating point value to set for the option.</param>
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, float single)
{
return Api is not null && Api->SetCaptureOptionF32(option, single) != 0;
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <param name="integer">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, <see cref="int.MaxValue"/>.</param>
[RenderDocApiVersion(1, 0)]
public static void GetCaptureOption(CaptureOption option, out uint integer)
{
integer = Api->GetCaptureOptionU32(option);
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <param name="single">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, -<see cref="float.MaxValue"/>.</param>
[RenderDocApiVersion(1, 0)]
public static void GetCaptureOption(CaptureOption option, out float single)
{
single = Api->GetCaptureOptionF32(option);
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>,
/// converted to a boolean.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, converted to bool, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns null.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool? GetCaptureOptionBool(CaptureOption option)
{
if (Api is null) return false;
uint returnVal = GetCaptureOptionU32(option);
if (returnVal == uint.MaxValue)
return null;
return returnVal is not 0;
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns <see cref="int.MaxValue"/>.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option);
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns -<see cref="float.MaxValue"/>.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option);
/// <summary>
/// Changes the key bindings in-application for changing the focussed window.
/// </summary>
/// <param name="buttons">lists the keys to bind.</param>
[RenderDocApiVersion(1, 0)]
public static void SetFocusToggleKeys(ReadOnlySpan<InputButton> buttons)
{
if (Api is null) return;
fixed (InputButton* ptr = buttons)
{
Api->SetFocusToggleKeys(ptr, buttons.Length);
}
}
/// <summary>
/// Changes the key bindings in-application for triggering a capture on the current window.
/// </summary>
/// <param name="buttons">lists the keys to bind.</param>
[RenderDocApiVersion(1, 0)]
public static void SetCaptureKeys(ReadOnlySpan<InputButton> buttons)
{
if (Api is null) return;
fixed (InputButton* ptr = buttons)
{
Api->SetCaptureKeys(ptr, buttons.Length);
}
}
/// <summary>
/// Attempts to remove RenderDoc and its hooks from the target process.<br/>
/// It must be called as early as possible in the process, and will have undefined results
/// if any graphics API functions have been called.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void RemoveHooks()
{
if (Api is null) return;
Api->RemoveHooks();
}
/// <summary>
/// Remove RenderDoc’s crash handler from the target process.<br/>
/// If you have your own crash handler that you want to handle any exceptions,
/// RenderDoc’s handler could interfere; so it can be disabled.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void UnloadCrashHandler()
{
if (Api is null) return;
Api->UnloadCrashHandler();
}
/// <summary>
/// Trigger a capture as if the user had pressed one of the capture hotkeys.<br/>
/// The capture will be taken from the next frame presented to whichever window is considered current.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void TriggerCapture()
{
if (Api is null) return;
Api->TriggerCapture();
}
/// <summary>
/// Gets the details of all frame capture in the current session.
/// This simply calls <see cref="GetCapture"/> for each index available as specified by <see cref="CaptureCount"/>.
/// </summary>
/// <returns>An immutable array of structs representing RenderDoc Captures.</returns>
public static ImmutableArray<Capture> GetCaptures()
{
if (Api is null) return [];
int captureCount = CaptureCount;
if (captureCount is 0) return [];
ImmutableArray<Capture>.Builder captures = ImmutableArray.CreateBuilder<Capture>(captureCount);
for (int captureIndex = 0; captureIndex < captureCount; captureIndex++)
{
if (GetCapture(captureIndex) is { } capture)
captures.Add(capture);
}
return captures.DrainToImmutable();
}
/// <summary>
/// Gets the details of a particular frame capture, as specified by an index from 0 to <see cref="CaptureCount"/> - 1.
/// </summary>
/// <param name="index">specifies which capture to return the details of. Must be less than the value returned by <see cref="CaptureCount"/>.</param>
/// <returns>A struct representing a RenderDoc Capture.</returns>
[RenderDocApiVersion(1, 0)]
public static Capture? GetCapture(int index)
{
if (Api is null) return null;
int length = 0;
if (Api->GetCapture(index, null, &length, null) == 0)
{
return null;
}
Span<byte> bytes = stackalloc byte[length + 1];
long timestamp;
fixed (byte* ptr = bytes)
Api->GetCapture(index, ptr, &length, &timestamp);
string fileName = Encoding.UTF8.GetString(bytes[length..]);
return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime);
}
/// <summary>
/// Determine the closest matching replay UI executable for the current RenderDoc module, and launch it.
/// </summary>
/// <param name="connectTargetControl">if the UI should immediately connect to the application.</param>
/// <param name="commandLine">string to be appended to the command line, e.g. a capture filename. If this parameter is null, the command line will be unmodified.</param>
/// <returns>true if the UI was successfully launched; false otherwise.</returns>
[RenderDocApiVersion(1, 0)]
public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine = null)
{
if (Api is null) return false;
if (commandLine == null)
{
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, null) != 0;
}
fixed (byte* ptr = commandLine.ToNullTerminatedByteArray())
{
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, ptr) != 0;
}
}
/// <summary>
/// Explicitly sets which window is considered active.<br/>
/// The active window is the one that will be captured when the keybind to trigger a capture is pressed.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. Must be valid.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. Must be valid.</param>
[RenderDocApiVersion(1, 0)]
public static void SetActiveWindow(nint hDevice, nint hWindow)
{
if (Api is null) return;
Api->SetActiveWindow((void*)hDevice, (void*)hWindow);
}
/// <summary>
/// Immediately begin a capture for the specified device/window combination.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
[RenderDocApiVersion(1, 0)]
public static void StartFrameCapture(nint hDevice, nint hWindow)
{
if (Api is null) return;
Api->StartFrameCapture((void*)hDevice, (void*)hWindow);
}
/// <summary>
/// Immediately end an active capture for the specified device/window combination.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <returns>true if the capture succeeded; false otherwise.</returns>
[RenderDocApiVersion(1, 0)]
public static bool EndFrameCapture(nint hDevice, nint hWindow)
{
if (Api is null) return false;
return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0;
}
/// <summary>
/// Trigger multiple sequential frame captures as if the user had pressed one of the capture hotkeys before each frame.<br/>
/// The captures will be taken from the next frames presented to whichever window is considered current.<br/>
/// Each capture will be taken independently and saved to a separate file, with no reference to the other frames.
/// </summary>
/// <param name="numFrames">the number of frames to capture.</param>
/// <remarks>Requires RenderDoc API version 1.1</remarks>
[RenderDocApiVersion(1, 1)]
public static void TriggerMultiFrameCapture(uint numFrames)
{
if (Api is null) return;
AssertAtLeast(1, 1);
Api->TriggerMultiFrameCapture(numFrames);
}
/// <summary>
/// Adds an arbitrary comments field to the most recent capture,
/// which will then be displayed in the UI to anyone opening the capture.
/// <br/><br/>
/// This is equivalent to calling <see cref="SetCaptureFileComments"/> with a null first (fileName) parameter.
/// </summary>
/// <param name="comments">the comments to set in the capture file.</param>
/// <remarks>Requires RenderDoc API version 1.2</remarks>
public static void SetMostRecentCaptureFileComments(string comments)
{
if (Api is null) return;
AssertAtLeast(1, 2);
byte[] commentBytes = comments.ToNullTerminatedByteArray();
fixed (byte* pcomment = commentBytes)
{
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
}
}
/// <summary>
/// Adds an arbitrary comments field to an existing capture on disk,
/// which will then be displayed in the UI to anyone opening the capture.
/// </summary>
/// <param name="fileName">the path to the capture file to set comments in. If this path is null or an empty string, the most recent capture file that has been created will be used.</param>
/// <param name="comments">the comments to set in the capture file.</param>
/// <remarks>Requires RenderDoc API version 1.2</remarks>
[RenderDocApiVersion(1, 2)]
public static void SetCaptureFileComments(string? fileName, string comments)
{
if (Api is null) return;
AssertAtLeast(1, 2);
byte[] commentBytes = comments.ToNullTerminatedByteArray();
fixed (byte* pcomment = commentBytes)
{
if (fileName is null)
{
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
}
else
{
byte[] fileBytes = fileName.ToNullTerminatedByteArray();
fixed (byte* pfile = fileBytes)
{
Api->SetCaptureFileComments(pfile, pcomment);
}
}
}
}
/// <summary>
/// Similar to <see cref="EndFrameCapture"/>, but the capture contents will be discarded immediately, and not processed and written to disk.<br/>
/// This will be more efficient than <see cref="EndFrameCapture"/> if the frame capture is not needed.
/// </summary>
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <returns>true if the capture was discarded; false if there was an error or no capture was in progress.</returns>
/// <remarks>Requires RenderDoc API version 1.4</remarks>
[RenderDocApiVersion(1, 4)]
public static bool DiscardFrameCapture(nint hDevice, nint hWindow)
{
if (Api is null) return false;
AssertAtLeast(1, 4);
return Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow) != 0;
}
/// <summary>
/// Requests that the currently connected replay UI raise its window to the top.<br/>
/// This is only possible if an instance of the replay UI is currently connected, otherwise this method does nothing.<br/>
/// This can be used in conjunction with <see cref="IsTargetControlConnected"/> and <see cref="LaunchReplayUI"/>,<br/> to intelligently handle showing the UI after making a capture.<br/><br/>
/// Given OS differences, it is not guaranteed that the UI will be successfully raised even if the request is passed on.<br/>
/// On some systems it may only be highlighted or otherwise indicated to the user.
/// </summary>
/// <returns>true if the request was passed onto the UI successfully; false if there is no UI connected or some other error occurred.</returns>
/// <remarks>Requires RenderDoc API version 1.5</remarks>
[RenderDocApiVersion(1, 5)]
public static bool ShowReplayUI()
{
if (Api is null) return false;
AssertAtLeast(1, 5);
return Api->ShowReplayUI() != 0;
}
/// <summary>
/// Sets a given title for the currently in-progress capture, which will be displayed in the UI.<br/>
/// This can be used either with a user-defined capture using a manual start and end,
/// or an automatic capture triggered by <see cref="TriggerCapture"/> or a keypress.<br/>
/// If multiple captures are ongoing at once, the title will be applied to the first capture to end only.<br/>
/// Any subsequent captures will not get any title unless the function is called again.
/// This function can only be called while a capture is in-progress,
/// after <see cref="StartFrameCapture"/> and before <see cref="EndFrameCapture"/>.<br/>
/// If it is called elsewhere it will have no effect.
/// If it is called multiple times within a capture, only the last title will have any effect.
/// </summary>
/// <param name="title">The title to set for the in-progress capture.</param>
/// <remarks>Requires RenderDoc API version 1.6</remarks>
[RenderDocApiVersion(1, 6)]
public static void SetCaptureTitle(string title)
{
if (Api is null) return;
AssertAtLeast(1, 6);
fixed (byte* ptr = title.ToNullTerminatedByteArray())
Api->SetCaptureTitle(ptr);
}
#region Dynamic Library loading
/// <summary>
/// Reload the internal RenderDoc API structure. Useful for manually refreshing <see cref="Api"/> while using process injection.
/// </summary>
/// <param name="ignoreAlreadyLoaded">Ignores the existing API function structure and overwrites it with a re-request.</param>
/// <param name="requiredVersion">The version of the RenderDoc API required by your application.</param>
public static void ReloadApi(bool ignoreAlreadyLoaded = false, RenderDocVersion? requiredVersion = null)
{
if (_loaded && !ignoreAlreadyLoaded)
return;
lock (typeof(RenderDoc))
{
// Prevent double loads.
if (_loaded && !ignoreAlreadyLoaded)
return;
if (requiredVersion.HasValue)
MinimumRequired = requiredVersion.Value;
_loaded = true;
_api = GetApi(MinimumRequired);
if (_api != null)
AssertAtLeast(MinimumRequired);
}
}
private static RenderDocApi* _api = null;
private static bool _loaded;
private static RenderDocApi* Api
{
get
{
ReloadApi();
return _api;
}
}
private static readonly Regex _dynamicLibraryPattern = RenderDocApiDynamicLibraryRegex();
private static RenderDocApi* GetApi(RenderDocVersion minimumRequired = RenderDocVersion.Version_1_0_0)
{
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
string moduleName = module.FileName ?? string.Empty;
if (!_dynamicLibraryPattern.IsMatch(moduleName))
continue;
if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle))
return null;
if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out nint procAddress))
return null;
var RENDERDOC_GetApi = (delegate* unmanaged[Cdecl]<RenderDocVersion, RenderDocApi**, int>)procAddress;
RenderDocApi* api;
return RENDERDOC_GetApi(minimumRequired, &api) != 0 ? api : null;
}
return null;
}
private static void AssertAtLeast(RenderDocVersion rdv, [CallerMemberName] string callee = "")
{
Version ver = rdv.SystemVersion;
AssertAtLeast(ver.Major, ver.Minor, ver.Build, callee);
}
private static void AssertAtLeast(int major, int minor, int patch = 0, [CallerMemberName] string callee = "")
{
if (!AssertVersionEnabled)
return;
if (Version!.Major < major)
goto fail;
if (Version.Major > major)
goto success;
if (Version.Minor < minor)
goto fail;
if (Version.Minor > minor)
goto success;
if (Version.Build < patch)
goto fail;
success:
return;
fail:
Version minVersion =
typeof(RenderDoc).GetMethod(callee)!.GetCustomAttribute<RenderDocApiVersionAttribute>()!.MinVersion;
throw new NotSupportedException(
$"This API was introduced in RenderDoc API {minVersion}. Current API version is {Version}.");
}
private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
return encoding.GetBytes(str + '\0');
}
[GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex RenderDocApiDynamicLibraryRegex();
#endregion
}
}
@@ -0,0 +1,51 @@
namespace Ryujinx.Graphics.RenderDocApi
{
#pragma warning disable CS0649
internal unsafe struct RenderDocApi
{
public delegate* unmanaged[Cdecl]<int*, int*, int*, void> GetApiVersion;
public delegate* unmanaged[Cdecl]<CaptureOption, uint, int> SetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float, int> SetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<CaptureOption, uint> GetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float> GetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetFocusToggleKeys;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetCaptureKeys;
public delegate* unmanaged[Cdecl]<OverlayBits> GetOverlayBits;
public delegate* unmanaged[Cdecl]<OverlayBits, OverlayBits, void> MaskOverlayBits;
public delegate* unmanaged[Cdecl]<void> RemoveHooks;
public delegate* unmanaged[Cdecl]<void> UnloadCrashHandler;
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<byte*> GetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<int> GetNumCaptures;
public delegate* unmanaged[Cdecl]<int, byte*, int*, long*, uint> GetCapture;
public delegate* unmanaged[Cdecl]<void> TriggerCapture;
public delegate* unmanaged[Cdecl]<uint> IsTargetControlConnected;
public delegate* unmanaged[Cdecl]<uint, byte*, uint> LaunchReplayUI;
public delegate* unmanaged[Cdecl]<void*, void*, void> SetActiveWindow;
public delegate* unmanaged[Cdecl]<void*, void*, void> StartFrameCapture;
public delegate* unmanaged[Cdecl]<uint> IsFrameCapturing;
public delegate* unmanaged[Cdecl]<void*, void*, uint> EndFrameCapture;
// 1.1
public delegate* unmanaged[Cdecl]<uint, void> TriggerMultiFrameCapture;
// 1.2
public delegate* unmanaged[Cdecl]<byte*, byte*, void> SetCaptureFileComments;
// 1.3
public delegate* unmanaged[Cdecl]<void*, void*, uint> DiscardFrameCapture;
// 1.5
public delegate* unmanaged[Cdecl]<uint> ShowReplayUI;
// 1.6
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureTitle;
}
#pragma warning restore CS0649
}
@@ -0,0 +1,16 @@
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public sealed class RenderDocApiVersionAttribute : Attribute
{
public Version MinVersion { get; }
public RenderDocApiVersionAttribute(int major, int minor, int patch = 0)
{
MinVersion = new Version(major, minor, patch);
}
}
}
@@ -0,0 +1,47 @@
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
public enum RenderDocVersion
{
Version_1_0_0 = 10000,
Version_1_0_1 = 10001,
Version_1_0_2 = 10002,
Version_1_1_0 = 10100,
Version_1_1_1 = 10101,
Version_1_1_2 = 10102,
Version_1_2_0 = 10200,
Version_1_3_0 = 10300,
Version_1_4_0 = 10400,
Version_1_4_1 = 10401,
Version_1_4_2 = 10402,
Version_1_5_0 = 10500,
Version_1_6_0 = 10600,
}
public static partial class Helpers
{
extension(RenderDocVersion rdv)
{
public Version SystemVersion
{
get
{
int i = (int)rdv;
return new (i / 10000, (i % 10000) / 100, i % 100);
}
}
}
extension(Version sv)
{
public RenderDocVersion RenderDocVersion
{
get
{
return (RenderDocVersion)(sv.Major * 10000 + sv.Minor * 100 + sv.Build);
}
}
}
}
}
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
@@ -1790,7 +1790,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{ {
(_, varType) = IoMap.GetSpirvBuiltIn(ioVariable); (_, varType) = IoMap.GetSpirvBuiltIn(ioVariable);
if (IoMap.IsPerVertexBuiltIn(ioVariable)) if (context.Definitions.Stage.IsVtg() && IoMap.IsPerVertexBuiltIn(ioVariable))
{ {
perVertexBuiltIn = ioVariable; perVertexBuiltIn = ioVariable;
ioVariable = IoVariable.Position; ioVariable = IoVariable.Position;
@@ -70,19 +70,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
Operand bindlessHandle = texOp.GetSource(0); Operand bindlessHandle = texOp.GetSource(0);
if (bindlessHandle.AsgOp is PhiNode phi) if (!IsBindlessAccessAllowed(bindlessHandle))
{
for (int srcIndex = 0; srcIndex < phi.SourcesCount; srcIndex++)
{
Operand phiSource = phi.GetSource(srcIndex);
if (phiSource.AsgOp is not PhiNode && !IsBindlessAccessAllowed(phiSource))
{
return false;
}
}
}
else if (!IsBindlessAccessAllowed(bindlessHandle))
{ {
return false; return false;
} }
@@ -100,7 +88,10 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
texOp.SetSource(0, textureIndex); texOp.SetSource(0, textureIndex);
bool hasSampler = !texOp.Inst.IsImage(); // Buffer textures use a TIC/BufferView only and never consume a sampler.
// Avoiding a sampler descriptor array here is especially important for bindless
// texture buffers, where the array can span the entire sampler pool for no benefit.
bool hasSampler = !texOp.Inst.IsImage() && texOp.Type != SamplerType.TextureBuffer;
SetBindingPair textureSetAndBinding = resourceManager.GetTextureOrImageBinding( SetBindingPair textureSetAndBinding = resourceManager.GetTextureOrImageBinding(
texOp.Inst, texOp.Inst,
@@ -143,26 +134,112 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
private static bool IsBindlessAccessAllowed(Operand bindlessHandle) private static bool IsBindlessAccessAllowed(Operand bindlessHandle)
{ {
if (bindlessHandle.Type == OperandType.ConstantBuffer) // Walk only SSA merges and integer operations that can transparently construct
{ // or select a packed texture/sampler handle. Do not walk arbitrary operations:
// Bindless access with handles from constant buffer is allowed. // finding an unrelated resource load elsewhere in the SSA graph must not enable
// descriptor-array access for this handle.
const int MaxVisitedOperands = 256;
const int MaxDepth = 64;
return true; Stack<(Operand Operand, int Depth)> work = new();
HashSet<Operand> visited = new();
work.Push((bindlessHandle, 0));
while (work.Count != 0 && visited.Count < MaxVisitedOperands)
{
(Operand operand, int depth) = work.Pop();
if (operand == null || depth > MaxDepth || !visited.Add(operand))
{
continue;
}
if (operand.Type == OperandType.ConstantBuffer)
{
// Constant buffers are accepted by the existing direct-handle path and
// remain accepted when SSA merges or integer handle arithmetic obscure them.
return true;
}
if (operand.AsgOp is PhiNode phi)
{
for (int index = 0; index < phi.SourcesCount; index++)
{
Operand source = phi.GetSource(index);
if (source.Type != OperandType.Undefined)
{
work.Push((source, depth + 1));
}
}
continue;
}
if (operand.AsgOp is not Operation operation)
{
continue;
}
Instruction inst = operation.Inst & Instruction.Mask;
if (inst == Instruction.Load)
{
// Preserve the performance restriction. Static CBUF operands are already
// accepted above; accept their dynamically indexed Load form as the same
// resource class, plus the existing shader-input and storage-buffer sources.
// Other loads remain traversal barriers and cannot authorize a pool array.
if (operation.StorageKind == StorageKind.ConstantBuffer ||
operation.StorageKind == StorageKind.Input ||
operation.StorageKind == StorageKind.StorageBuffer)
{
return true;
}
continue;
}
if (!IsHandleConstructionOperation(inst))
{
// Texture/image operations, arbitrary loads, floating-point conversions,
// calls and other unrelated calculations are deliberate traversal barriers.
continue;
}
for (int index = 0; index < operation.SourcesCount; index++)
{
work.Push((operation.GetSource(index), depth + 1));
}
} }
if (bindlessHandle.AsgOp is not Operation handleOp || return false;
handleOp.Inst != Instruction.Load || }
(handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
{
// Right now, we only allow bindless access when the handle comes from a shader input or storage buffer.
// This is an artificial limitation to prevent it from being used in cases where it
// would have a large performance impact of loading all textures in the pool.
// It might be removed in the future, if we can mitigate the performance impact.
return false; private static bool IsHandleConstructionOperation(Instruction inst)
} {
return inst is
return true; Instruction.Add or
Instruction.Subtract or
Instruction.Multiply or
Instruction.MultiplyHighS32 or
Instruction.MultiplyHighU32 or
Instruction.BitwiseAnd or
Instruction.BitwiseExclusiveOr or
Instruction.BitwiseNot or
Instruction.BitwiseOr or
Instruction.BitfieldExtractS32 or
Instruction.BitfieldExtractU32 or
Instruction.BitfieldInsert or
Instruction.ShiftLeft or
Instruction.ShiftRightS32 or
Instruction.ShiftRightU32 or
Instruction.MinimumU32 or
Instruction.MaximumU32 or
Instruction.ClampU32 or
Instruction.ConditionalSelect or
Instruction.Copy or
Instruction.VectorExtract;
} }
private static bool TryConvertBindless(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor, TextureOperation texOp) private static bool TryConvertBindless(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor, TextureOperation texOp)
@@ -32,6 +32,29 @@ namespace Ryujinx.Graphics.Vulkan
public bool HasDepthStencil { get; private set; } public bool HasDepthStencil { get; private set; }
public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0); public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0);
public bool SetVirtualSize(uint width, uint height, uint layers)
{
if (AttachmentsCount != 0)
{
return false;
}
width = Math.Max(1u, width);
height = Math.Max(1u, height);
layers = Math.Max(1u, layers);
if (Width == width && Height == height && Layers == layers)
{
return false;
}
Width = width;
Height = height;
Layers = layers;
return true;
}
public FramebufferParams(Device device, TextureView view, uint width, uint height) public FramebufferParams(Device device, TextureView view, uint width, uint height)
{ {
Format format = view.Info.Format; Format format = view.Info.Format;
+4 -4
View File
@@ -406,10 +406,10 @@ namespace Ryujinx.Graphics.Vulkan
if (dstIsDepthOrStencil) if (dstIsDepthOrStencil)
{ {
_pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programDepthBlitMs : _programDepthBlit); _pipeline.SetProgram(src.Info.Target.IsMultisample ? _programDepthBlitMs : _programDepthBlit);
_pipeline.SetDepthTest(new DepthTestDescriptor(true, true, CompareOp.Always)); _pipeline.SetDepthTest(new DepthTestDescriptor(true, true, CompareOp.Always));
} }
else if (src.Info.Target.IsMultisample()) else if (src.Info.Target.IsMultisample)
{ {
_pipeline.SetProgram(_programColorBlitMs); _pipeline.SetProgram(_programColorBlitMs);
} }
@@ -566,12 +566,12 @@ namespace Ryujinx.Graphics.Vulkan
if (isDepth) if (isDepth)
{ {
_pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programDepthBlitMs : _programDepthBlit); _pipeline.SetProgram(src.Info.Target.IsMultisample ? _programDepthBlitMs : _programDepthBlit);
_pipeline.SetDepthTest(new DepthTestDescriptor(true, true, CompareOp.Always)); _pipeline.SetDepthTest(new DepthTestDescriptor(true, true, CompareOp.Always));
} }
else else
{ {
_pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programStencilBlitMs : _programStencilBlit); _pipeline.SetProgram(src.Info.Target.IsMultisample ? _programStencilBlitMs : _programStencilBlit);
_pipeline.SetStencilTest(CreateStencilTestDescriptor(true)); _pipeline.SetStencilTest(CreateStencilTestDescriptor(true));
} }
+32
View File
@@ -0,0 +1,32 @@
using Silk.NET.Vulkan;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Vulkan
{
public static class Helpers
{
extension(Vk api)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Vk"/>'s <see cref="Instance"/> pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer() =>
api.CurrentInstance is not null
? api.CurrentInstance.Value.GetRenderDocDevicePointer()
: null;
}
extension(Instance instance)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Instance"/>'s pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer()
=> (*((void**)(instance.Handle)));
}
}
}
@@ -843,6 +843,11 @@ namespace Ryujinx.Graphics.Vulkan
public void SetImage(ShaderStage stage, int binding, ITexture image) public void SetImage(ShaderStage stage, int binding, ITexture image)
{ {
_descriptorSetUpdater.SetImage(Cbs, stage, binding, image); _descriptorSetUpdater.SetImage(Cbs, stage, binding, image);
if (stage == ShaderStage.Fragment && image is TextureView view)
{
FramebufferParams?.SetVirtualSize((uint)view.Width, (uint)view.Height, (uint)view.Layers);
}
} }
public void SetImage(int binding, Auto<DisposableImageView> image) public void SetImage(int binding, Auto<DisposableImageView> image)
@@ -1607,6 +1612,24 @@ namespace Ryujinx.Graphics.Vulkan
private bool RecreateGraphicsPipelineIfNeeded() private bool RecreateGraphicsPipelineIfNeeded()
{ {
if (FramebufferParams != null &&
FramebufferParams.AttachmentsCount == 0 &&
FramebufferParams.Width == 1 &&
FramebufferParams.Height == 1 &&
DynamicState.ViewportsCount != 0)
{
// An attachmentless fragment pass can reach its first draw before the storage
// image descriptor is rebound. At that point the null framebuffer still has
// its constructor fallback of 1x1, even though the guest viewport already
// describes the real render area. Seed the virtual framebuffer from that
// viewport so the first storage-image draw is not clipped to one pixel.
Silk.NET.Vulkan.Viewport viewport = DynamicState.Viewports[0];
uint width = (uint)Math.Max(1f, Math.Abs(viewport.Width));
uint height = (uint)Math.Max(1f, Math.Abs(viewport.Height));
FramebufferParams.SetVirtualSize(width, height, 1);
}
if (AutoFlush.ShouldFlushDraw(DrawCount)) if (AutoFlush.ShouldFlushDraw(DrawCount))
{ {
Gd.FlushAllCommands(); Gd.FlushAllCommands();
@@ -79,7 +79,7 @@ namespace Ryujinx.Graphics.Vulkan
_device = device; _device = device;
_info = info; _info = info;
bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample(); bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample;
VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported); VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported);
uint levels = (uint)info.Levels; uint levels = (uint)info.Levels;
@@ -323,7 +323,7 @@ namespace Ryujinx.Graphics.Vulkan
usage |= ImageUsageFlags.ColorAttachmentBit; usage |= ImageUsageFlags.ColorAttachmentBit;
} }
if (format.IsImageCompatible() && (isMsImageStorageSupported || !target.IsMultisample())) if (format.IsImageCompatible() && (isMsImageStorageSupported || !target.IsMultisample))
{ {
usage |= ImageUsageFlags.StorageBit; usage |= ImageUsageFlags.StorageBit;
} }
+6 -6
View File
@@ -61,7 +61,7 @@ namespace Ryujinx.Graphics.Vulkan
gd.Textures.Add(this); gd.Textures.Add(this);
bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample(); bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample;
VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported); VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported);
ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, info.Target, gd.Capabilities, isMsImageStorageSupported) & storage.UsageFlags; ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, info.Target, gd.Capabilities, isMsImageStorageSupported) & storage.UsageFlags;
@@ -126,7 +126,7 @@ namespace Ryujinx.Graphics.Vulkan
ImageUsageFlags shaderUsage = ImageUsageFlags.SampledBit; ImageUsageFlags shaderUsage = ImageUsageFlags.SampledBit;
if (info.Format.IsImageCompatible() && (_gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample())) if (info.Format.IsImageCompatible() && (_gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample))
{ {
shaderUsage |= ImageUsageFlags.StorageBit; shaderUsage |= ImageUsageFlags.StorageBit;
} }
@@ -225,12 +225,12 @@ namespace Ryujinx.Graphics.Vulkan
Image srcImage = src.GetImage().Get(cbs).Value; Image srcImage = src.GetImage().Get(cbs).Value;
Image dstImage = dst.GetImage().Get(cbs).Value; Image dstImage = dst.GetImage().Get(cbs).Value;
if (!dst.Info.Target.IsMultisample() && Info.Target.IsMultisample()) if (!dst.Info.Target.IsMultisample && Info.Target.IsMultisample)
{ {
int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer); int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer);
_gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, 0, firstLayer, layers); _gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, 0, firstLayer, layers);
} }
else if (dst.Info.Target.IsMultisample() && !Info.Target.IsMultisample()) else if (dst.Info.Target.IsMultisample && !Info.Target.IsMultisample)
{ {
int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer); int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer);
_gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, 0, firstLayer, layers); _gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, 0, firstLayer, layers);
@@ -287,11 +287,11 @@ namespace Ryujinx.Graphics.Vulkan
Image srcImage = src.GetImage().Get(cbs).Value; Image srcImage = src.GetImage().Get(cbs).Value;
Image dstImage = dst.GetImage().Get(cbs).Value; Image dstImage = dst.GetImage().Get(cbs).Value;
if (!dst.Info.Target.IsMultisample() && Info.Target.IsMultisample()) if (!dst.Info.Target.IsMultisample && Info.Target.IsMultisample)
{ {
_gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1); _gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1);
} }
else if (dst.Info.Target.IsMultisample() && !Info.Target.IsMultisample()) else if (dst.Info.Target.IsMultisample && !Info.Target.IsMultisample)
{ {
_gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1); _gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1);
} }
+3
View File
@@ -27,6 +27,9 @@ namespace Ryujinx.Graphics.Vulkan
[GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")] [GeneratedRegex("NVIDIA GeForce (R|G)?TX? (\\d{3}\\d?)M?")]
public static partial Regex NvidiaConsumerClassRegex(); public static partial Regex NvidiaConsumerClassRegex();
[GeneratedRegex(@"^\d+\.\d+\.\d+\.\d+$")]
internal static partial Regex IntelWindowsDriverVersionRegex();
public static Vendor FromId(uint id) public static Vendor FromId(uint id)
{ {
return id switch return id switch
+62 -3
View File
@@ -105,6 +105,7 @@ namespace Ryujinx.Graphics.Vulkan
public string GpuDriver { get; private set; } public string GpuDriver { get; private set; }
public string GpuRenderer { get; private set; } public string GpuRenderer { get; private set; }
public string GpuVersion { get; private set; } public string GpuVersion { get; private set; }
public string GpuDriverVersion { get; private set; }
public bool PreferThreading => true; public bool PreferThreading => true;
@@ -177,6 +178,14 @@ namespace Ryujinx.Graphics.Vulkan
SType = StructureType.PhysicalDeviceProperties2, SType = StructureType.PhysicalDeviceProperties2,
}; };
PhysicalDeviceIDProperties propertiesId = new()
{
SType = StructureType.PhysicalDeviceIDProperties,
PNext = properties2.PNext,
};
properties2.PNext = &propertiesId;
PhysicalDeviceSubgroupProperties propertiesSubgroup = new() PhysicalDeviceSubgroupProperties propertiesSubgroup = new()
{ {
SType = StructureType.PhysicalDeviceSubgroupProperties, SType = StructureType.PhysicalDeviceSubgroupProperties,
@@ -367,13 +376,17 @@ namespace Ryujinx.Graphics.Vulkan
GpuVendor = VendorUtils.GetNameFromId(properties.VendorID); GpuVendor = VendorUtils.GetNameFromId(properties.VendorID);
GpuDriver = hasDriverProperties && !OperatingSystem.IsMacOS() ? GpuDriver = hasDriverProperties && !OperatingSystem.IsMacOS() ?
VendorUtils.GetFriendlyDriverName(driverProperties.DriverID) : GpuVendor; // Fallback to vendor name if driver is unavailable or on MacOS where vendor is preferred. VendorUtils.GetFriendlyDriverName(driverProperties.DriverID) : GpuVendor; // Fallback to vendor name if driver is unavailable or on MacOS where vendor is preferred.
GpuDriverVersion = TryGetIntelWindowsDriverVersionFromUuid(ref propertiesId, out string intelDriverVersion) ? intelDriverVersion :
hasDriverProperties ?
GetDriverInfo(ref driverProperties) ?? ParseDriverVersion(ref properties) :
ParseDriverVersion(ref properties);
fixed (byte* deviceName = properties.DeviceName) fixed (byte* deviceName = properties.DeviceName)
{ {
GpuRenderer = Marshal.PtrToStringAnsi((nint)deviceName); GpuRenderer = Marshal.PtrToStringAnsi((nint)deviceName);
} }
GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}, Driver v{ParseDriverVersion(ref properties)}"; GpuVersion = $"Vulkan v{ParseStandardVulkanVersion(properties.ApiVersion)}";
IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer); IsAmdGcn = !IsMoltenVk && Vendor == Vendor.Amd && VendorUtils.AmdGcnRegex().IsMatch(GpuRenderer);
@@ -824,7 +837,7 @@ namespace Ryujinx.Graphics.Vulkan
public HardwareInfo GetHardwareInfo() public HardwareInfo GetHardwareInfo()
{ {
return new HardwareInfo(GpuVendor, GpuRenderer, GpuDriver); return new HardwareInfo(GpuVendor, GpuRenderer, GpuDriver, GpuDriverVersion);
} }
/// <summary> /// <summary>
@@ -874,9 +887,55 @@ namespace Ryujinx.Graphics.Vulkan
return $"{(driverVersionRaw >> 22) & 0x3FF}.{(driverVersionRaw >> 14) & 0xFF}.{(driverVersionRaw >> 6) & 0xFF}.{driverVersionRaw & 0x3F}"; return $"{(driverVersionRaw >> 22) & 0x3FF}.{(driverVersionRaw >> 14) & 0xFF}.{(driverVersionRaw >> 6) & 0xFF}.{driverVersionRaw & 0x3F}";
} }
// Intel's Windows Vulkan driver exposes the 101.xxxx build portion in a custom layout.
if (properties.VendorID == 0x8086 && OperatingSystem.IsWindows())
{
return $"{driverVersionRaw >> 14}.{driverVersionRaw & 0x3FFF}";
}
return ParseStandardVulkanVersion(driverVersionRaw); return ParseStandardVulkanVersion(driverVersionRaw);
} }
private static unsafe string GetDriverInfo(ref PhysicalDeviceDriverPropertiesKHR driverProperties)
{
fixed (byte* driverInfo = driverProperties.DriverInfo)
{
string driverInfoString = Marshal.PtrToStringAnsi((nint)driverInfo);
return string.IsNullOrWhiteSpace(driverInfoString) ? null : driverInfoString;
}
}
private static unsafe bool TryGetIntelWindowsDriverVersionFromUuid(ref PhysicalDeviceIDProperties propertiesId, out string driverVersion)
{
driverVersion = null;
if (!OperatingSystem.IsWindows())
{
return false;
}
fixed (byte* driverUuid = propertiesId.DriverUuid)
{
string driverUuidString = Marshal.PtrToStringAnsi((nint)driverUuid, (int)Vk.UuidSize);
int terminatorIndex = driverUuidString.IndexOf('\0');
if (terminatorIndex >= 0)
{
driverUuidString = driverUuidString[..terminatorIndex];
}
if (VendorUtils.IntelWindowsDriverVersionRegex().IsMatch(driverUuidString))
{
driverVersion = driverUuidString;
return true;
}
}
return false;
}
internal PrimitiveTopology TopologyRemap(PrimitiveTopology topology) internal PrimitiveTopology TopologyRemap(PrimitiveTopology topology)
{ {
return topology switch return topology switch
@@ -902,7 +961,7 @@ namespace Ryujinx.Graphics.Vulkan
private void PrintGpuInformation() private void PrintGpuInformation()
{ {
Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion})"); Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion}, Driver: {GpuDriver} {GpuDriverVersion})");
Logger.Notice.Print(LogClass.Gpu, $"GPU Memory: {GetTotalGPUMemory() / (1024 * 1024)} MiB"); Logger.Notice.Print(LogClass.Gpu, $"GPU Memory: {GetTotalGPUMemory() / (1024 * 1024)} MiB");
} }
+6 -9
View File
@@ -1,4 +1,5 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Translation.PTC;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
@@ -9,12 +10,10 @@ namespace Ryujinx.HLE.HOS
interface IArmProcessContext : IProcessContext interface IArmProcessContext : IProcessContext
{ {
IDiskCacheLoadState Initialize( IDiskCacheLoadState Initialize(
string titleIdText, PtcCacheInfo cacheInfo,
string displayVersion,
bool diskCacheEnabled, bool diskCacheEnabled,
ulong codeAddress, ulong codeAddress,
ulong codeSize, ulong codeSize);
string cacheSelector);
} }
class ArmProcessContext<T> : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager class ArmProcessContext<T> : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager
@@ -64,15 +63,13 @@ namespace Ryujinx.HLE.HOS
} }
public IDiskCacheLoadState Initialize( public IDiskCacheLoadState Initialize(
string titleIdText, PtcCacheInfo cacheInfo,
string displayVersion,
bool diskCacheEnabled, bool diskCacheEnabled,
ulong codeAddress, ulong codeAddress,
ulong codeSize, ulong codeSize)
string cacheSelector)
{ {
_cpuContext.PrepareCodeRange(codeAddress, codeSize); _cpuContext.PrepareCodeRange(codeAddress, codeSize);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled, cacheSelector); return _cpuContext.LoadDiskCache(cacheInfo, diskCacheEnabled);
} }
public void InvalidateCacheRegion(ulong address, ulong size) public void InvalidateCacheRegion(ulong address, ulong size)
@@ -1,3 +1,4 @@
using ARMeilleure.Translation.PTC;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Cpu; using Ryujinx.Cpu;
@@ -7,6 +8,7 @@ using Ryujinx.Cpu.LightningJit;
using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -17,8 +19,10 @@ namespace Ryujinx.HLE.HOS
{ {
private readonly ITickSource _tickSource; private readonly ITickSource _tickSource;
private readonly GpuContext _gpu; private readonly GpuContext _gpu;
private readonly string _titleIdText; private readonly ulong _programId;
private readonly byte _programIndex;
private readonly string _displayVersion; private readonly string _displayVersion;
private readonly ProcessKind _processKind;
private readonly bool _diskCacheEnabled; private readonly bool _diskCacheEnabled;
private readonly string _diskCacheSelector; private readonly string _diskCacheSelector;
private readonly ulong _codeAddress; private readonly ulong _codeAddress;
@@ -29,8 +33,10 @@ namespace Ryujinx.HLE.HOS
public ArmProcessContextFactory( public ArmProcessContextFactory(
ITickSource tickSource, ITickSource tickSource,
GpuContext gpu, GpuContext gpu,
string titleIdText, ulong programId,
byte programIndex,
string displayVersion, string displayVersion,
ProcessKind processKind,
bool diskCacheEnabled, bool diskCacheEnabled,
string diskCacheSelector, string diskCacheSelector,
ulong codeAddress, ulong codeAddress,
@@ -38,8 +44,10 @@ namespace Ryujinx.HLE.HOS
{ {
_tickSource = tickSource; _tickSource = tickSource;
_gpu = gpu; _gpu = gpu;
_titleIdText = titleIdText; _programId = programId;
_programIndex = programIndex;
_displayVersion = displayVersion; _displayVersion = displayVersion;
_processKind = processKind;
_diskCacheEnabled = diskCacheEnabled; _diskCacheEnabled = diskCacheEnabled;
_diskCacheSelector = diskCacheSelector; _diskCacheSelector = diskCacheSelector;
_codeAddress = codeAddress; _codeAddress = codeAddress;
@@ -121,8 +129,18 @@ namespace Ryujinx.HLE.HOS
} }
string cacheSelector = _diskCacheSelector ?? "default"; string cacheSelector = _diskCacheSelector ?? "default";
string programIdText = _programId == 0 ? string.Empty : $"{_programId:x16}";
string applicationIdText = _programId == 0 ? string.Empty : $"{_programId & ~0xFul:x16}";
PtcCacheInfo cacheInfo = new(
pid,
programIdText,
applicationIdText,
_programIndex,
_displayVersion,
_processKind.ToString(),
cacheSelector);
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, cacheSelector); DiskCacheLoadState = processContext.Initialize(cacheInfo, _diskCacheEnabled, _codeAddress, _codeSize);
return processContext; return processContext;
} }
@@ -26,12 +26,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
{ {
MemoryArrange.MemoryArrange4GiB or MemoryArrange.MemoryArrange4GiB or
MemoryArrange.MemoryArrange4GiBSystemDev or MemoryArrange.MemoryArrange4GiBSystemDev or
MemoryArrange.MemoryArrange6GiBAppletDev => 3152 * MiB, MemoryArrange.MemoryArrange6GiBAppletDev => 3173 * MiB,
MemoryArrange.MemoryArrange4GiBAppletDev => 2048 * MiB, MemoryArrange.MemoryArrange4GiBAppletDev => 2048 * MiB,
MemoryArrange.MemoryArrange6GiB => 4783 * MiB, MemoryArrange.MemoryArrange6GiB => 4804 * MiB,
MemoryArrange.MemoryArrange8GiB => 6831 * MiB, MemoryArrange.MemoryArrange8GiB => 6852 * MiB,
MemoryArrange.MemoryArrange10GiB => 8879 * MiB, MemoryArrange.MemoryArrange10GiB => 8900 * MiB,
MemoryArrange.MemoryArrange12GiB => 10927 * MiB, MemoryArrange.MemoryArrange12GiB => 10948 * MiB,
_ => throw new ArgumentException($"Invalid memory arrange \"{arrange}\"."), _ => throw new ArgumentException($"Invalid memory arrange \"{arrange}\"."),
}; };
} }
@@ -15,7 +15,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private readonly Dictionary<ulong, List<KThread>> _condVarThreads; private readonly Dictionary<ulong, List<KThread>> _condVarThreads;
private readonly Dictionary<ulong, List<KThread>> _arbiterThreads; private readonly Dictionary<ulong, List<KThread>> _arbiterThreads;
private readonly ByDynamicPriority _byDynamicPriority;
public KAddressArbiter(KernelContext context) public KAddressArbiter(KernelContext context)
{ {
@@ -23,7 +22,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
_condVarThreads = []; _condVarThreads = [];
_arbiterThreads = []; _arbiterThreads = [];
_byDynamicPriority = new ByDynamicPriority();
} }
public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle) public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle)
@@ -142,14 +140,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (_condVarThreads.TryGetValue(condVarAddress, out List<KThread> threads)) if (_condVarThreads.TryGetValue(condVarAddress, out List<KThread> threads))
{ {
int i = 0; int i = FindDynamicPriorityFifoInsertionIndex(threads, currentThread);
if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread); threads.Insert(i, currentThread);
} }
else else
@@ -332,14 +325,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads)) if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
{ {
int i = 0; int i = FindDynamicPriorityFifoInsertionIndex(threads, currentThread);
if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread); threads.Insert(i, currentThread);
} }
else else
@@ -424,14 +412,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads)) if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
{ {
int i = 0; int i = FindDynamicPriorityFifoInsertionIndex(threads, currentThread);
if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread); threads.Insert(i, currentThread);
} }
else else
@@ -627,12 +610,28 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
return validCount; return validCount;
} }
private class ByDynamicPriority : IComparer<KThread> private static int FindDynamicPriorityFifoInsertionIndex(List<KThread> threads, KThread currentThread)
{ {
public int Compare(KThread x, KThread y) int low = 0;
int high = threads.Count;
// Lower numeric values represent higher priorities. Use upper-bound insertion
// to preserve FIFO order among waiters with the same dynamic priority.
while (low < high)
{ {
return x!.DynamicPriority.CompareTo(y!.DynamicPriority); int middle = low + ((high - low) >> 1);
if (threads[middle].DynamicPriority <= currentThread.DynamicPriority)
{
low = middle + 1;
}
else
{
high = middle;
}
} }
return low;
} }
} }
} }
+22 -2
View File
@@ -783,8 +783,28 @@ namespace Ryujinx.HLE.HOS
} }
List<Cheat> cheats = mods.Cheats; List<Cheat> cheats = mods.Cheats;
Dictionary<string, ulong> processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v }) Dictionary<string, ulong> processExes = new();
.ToDictionary(x => x.k[..Math.Min(Cheat.CheatIdSize, x.k.Length)], x => x.v);
foreach ((string buildId, ulong codeAddress) in tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses))
{
string normalizedBuildId = buildId[..Math.Min(Cheat.CheatIdSize, buildId.Length)];
if (processExes.TryGetValue(normalizedBuildId, out ulong existingAddress))
{
if (existingAddress != codeAddress)
{
Logger.Warning?.Print(
LogClass.ModLoader,
$"Duplicate BuildId prefix '{normalizedBuildId}' has different code addresses. " +
$"Existing: 0x{existingAddress:X}, duplicate: 0x{codeAddress:X}. " +
$"Keeping the first one.");
}
continue;
}
processExes.Add(normalizedBuildId, codeAddress);
}
foreach (Cheat cheat in cheats) foreach (Cheat cheat in cheats)
{ {
@@ -71,7 +71,14 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
return _applicationServiceServer.IsUserRegistrationRequestPermitted(context); return _applicationServiceServer.IsUserRegistrationRequestPermitted(context);
} }
[CommandCmif(51)] [CommandCmif(51)] // 1.0.0 – 18.1.0
// TrySelectUserWithoutInteraction(bool) -> nn::account::Uid
public ResultCode TrySelectUserWithoutInteractionDeprecated(ServiceCtx context)
{
return _applicationServiceServer.TrySelectUserWithoutInteraction(context);
}
[CommandCmif(52)] // 19.0.0+
// TrySelectUserWithoutInteraction(bool) -> nn::account::Uid // TrySelectUserWithoutInteraction(bool) -> nn::account::Uid
public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context) public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context)
{ {
@@ -12,12 +12,23 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
{ {
class ICommonStateGetter : DisposableIpcService class ICommonStateGetter : DisposableIpcService
{ {
// Nintendo serves the VR goggle commands based on Title ID, apparently.
private static ReadOnlySpan<ulong> _supportedVRGoggleTitles =>
[
0x0100CA001D972000, // Nintendo Classics: Virtual Boy NSO
0x0100BFC01D976000 // Nintendo Classics: Virtual Boy NSO
];
private int _resolutionWidth = 1280;
private int _resolutionHeight = 720;
private readonly ServiceCtx _context; private readonly ServiceCtx _context;
private readonly Apm.ManagerServer _apmManagerServer; private readonly Apm.ManagerServer _apmManagerServer;
private readonly Apm.SystemManagerServer _apmSystemManagerServer; private readonly Apm.SystemManagerServer _apmSystemManagerServer;
private bool _vrModeEnabled; private bool _vrModeEnabled;
private bool _vrMode3dEnabled;
#pragma warning disable CS0414, IDE0052 // Remove unread private member #pragma warning disable CS0414, IDE0052 // Remove unread private member
private bool _lcdBacklighOffEnabled; private bool _lcdBacklighOffEnabled;
private bool _requestExitToLibraryAppletAtExecuteNextProgramEnabled; private bool _requestExitToLibraryAppletAtExecuteNextProgramEnabled;
@@ -153,6 +164,47 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success; return ResultCode.Success;
} }
[CommandCmif(30)]
// GetHomeButtonReaderLockAccessor() -> nn::am::service::ILockAccessor
public ResultCode GetHomeButtonReaderLockAccessor(ServiceCtx context)
{
// We currently do not have any home button functionality, so it's fine to stub this for now.
// Similar to using GetReaderLockAccessorEx() with inval=0.
Logger.Stub?.PrintStub(LogClass.ServiceAm);
MakeObject(context, new ILockAccessor(context));
return ResultCode.Success;
}
[CommandCmif(31)] // 2.0.0 +
// GetReaderLockAccessorEx(uint unknown) -> nn::am::service::ILockAccessor
public ResultCode GetReaderLockAccessorEx(ServiceCtx context)
{
uint unknown = context.RequestData.ReadUInt32();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
if (unknown < 0 || unknown > 3)
{
throw new ArgumentOutOfRangeException(nameof(unknown));
}
MakeObject(context, new ILockAccessor(context));
return ResultCode.Success;
}
[CommandCmif(32)] // 2.0.0+
// GetWriterLockAccessorEx(uint unknown) -> nn::am::service::ILockAccessor
public ResultCode GetWriterLockAccessorEx(ServiceCtx context)
{
uint unknown = context.RequestData.ReadUInt32();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
if (unknown < 0 || unknown > 3)
{
throw new ArgumentOutOfRangeException(nameof(unknown));
}
MakeObject(context, new ILockAccessor(context));
return ResultCode.Success;
}
[CommandCmif(50)] // 3.0.0+ [CommandCmif(50)] // 3.0.0+
// IsVrModeEnabled() -> b8 // IsVrModeEnabled() -> b8
public ResultCode IsVrModeEnabled(ServiceCtx context) public ResultCode IsVrModeEnabled(ServiceCtx context)
@@ -177,8 +229,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
// SetLcdBacklighOffEnabled(b8) // SetLcdBacklighOffEnabled(b8)
public ResultCode SetLcdBacklighOffEnabled(ServiceCtx context) public ResultCode SetLcdBacklighOffEnabled(ServiceCtx context)
{ {
// NOTE: Service sets a private field here, maybe this field is used somewhere else to turned off the backlight. // NOTE: Service sets a private field here, maybe this field is used somewhere else to turn off the backlight.
// Since we don't support backlight, it's fine to do nothing. // Since we don't support the backlight feature, it's fine to stub it.
_lcdBacklighOffEnabled = context.RequestData.ReadBoolean(); _lcdBacklighOffEnabled = context.RequestData.ReadBoolean();
@@ -298,18 +350,43 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return (ResultCode)_apmSystemManagerServer.GetCurrentPerformanceConfiguration(context); return (ResultCode)_apmSystemManagerServer.GetCurrentPerformanceConfiguration(context);
} }
[CommandCmif(130)] // 21.0.0+
// EnableStartupLogoDisappearedMessage()
public ResultCode EnableStartupLogoDisappearedMessage(ServiceCtx context)
{
// NOTE: Service only delivers the message once the startup logo has actually
// disappeared. We never display one, so it is already deliverable. Callers
// gate their first frame on this message and hang without it.
AppletStateMgr appletState = context.Device.System.AppletState;
appletState.Messages.Enqueue(AppletMessage.StartupLogoDisappeared);
appletState.MessageEvent.ReadableEvent.Signal();
return ResultCode.Success;
}
[CommandCmif(300)] // 9.0.0+ [CommandCmif(300)] // 9.0.0+
// GetSettingsPlatformRegion() -> u8 // GetSettingsPlatformRegion() -> u8
public ResultCode GetSettingsPlatformRegion(ServiceCtx context) public ResultCode GetSettingsPlatformRegion(ServiceCtx context)
{ {
PlatformRegion platformRegion = context.Device.System.State.DesiredRegionCode == (uint)RegionCode.China ? PlatformRegion.China : PlatformRegion.Global; PlatformRegion platformRegion = context.Device.System.State.DesiredRegionCode == (uint)RegionCode.China ? PlatformRegion.China : PlatformRegion.Global;
// FIXME: Call set:sys GetPlatformRegion // FIXME: Call set:sys GetPlatformRegion
context.ResponseData.Write((byte)platformRegion); context.ResponseData.Write((byte)platformRegion);
return ResultCode.Success; return ResultCode.Success;
} }
[CommandCmif(610)] // 21.0.0+
// UnknownCommand610(long unknown)
public ResultCode UnknownCommand610(ServiceCtx context)
{
long unknown = context.RequestData.ReadInt64();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
return ResultCode.Success;
}
[CommandCmif(900)] // 11.0.0+ [CommandCmif(900)] // 11.0.0+
// SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled() // SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled()
public ResultCode SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled(ServiceCtx context) public ResultCode SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled(ServiceCtx context)
@@ -320,6 +397,100 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success; return ResultCode.Success;
} }
[CommandCmif(1000)] // 19.0.0+
// BeginVrMode3d()
public ResultCode BeginVrMode3d(ServiceCtx context)
{
// NOTE: Service also applies a stereo scale to the goggle display, which we don't
// model, so only the state is kept here.
_vrMode3dEnabled = true;
return ResultCode.Success;
}
[CommandCmif(1001)] // 19.0.0+
// EndVrMode3d()
public ResultCode EndVrMode3d(ServiceCtx context)
{
_vrMode3dEnabled = false;
return ResultCode.Success;
}
[CommandCmif(1002)] // 19.0.0+
// IsVrModeEnabled3d() -> b8
public ResultCode IsVrModeEnabled3d(ServiceCtx context)
{
context.ResponseData.Write(_vrMode3dEnabled);
return ResultCode.Success;
}
[CommandCmif(1003)] // 21.0.0+
// GetVrLaboGoggleViewport() -> (s32 x, s32 y, s32 width, s32 height)
public ResultCode GetVrLaboGoggleViewport(ServiceCtx context)
{
if (!IsVrLaboGoggleSupportedTitle(context))
{
return ResultCode.ObjectInvalid;
}
int VrDisplayCoordinateX = 0;
int VrDisplayCoordinateY = 0;
int VrDisplayWidth = _resolutionWidth;
int VrDisplayHeight = _resolutionHeight;
context.ResponseData.Write(VrDisplayCoordinateX);
context.ResponseData.Write(VrDisplayCoordinateY);
context.ResponseData.Write(VrDisplayWidth);
context.ResponseData.Write(VrDisplayHeight);
return ResultCode.Success;
}
[CommandCmif(1004)] // 21.0.0+
// GetPanelPhysicalSizeForSpecificTitle() -> (f32 width, f32 height)
public ResultCode GetPanelPhysicalSizeForSpecificTitle(ServiceCtx context)
{
if (!IsVrLaboGoggleSupportedTitle(context))
{
return ResultCode.ObjectInvalid;
}
// The switch provides micrometres, but the command reports millimetres.
// 6.2 inch 16:9 panel.
float PanelPhysicalWidthMicroMeters = 137250f;
float PanelPhysicalHeightMicroMeters = 77200f;
context.ResponseData.Write(PanelPhysicalWidthMicroMeters / 1000f);
context.ResponseData.Write(PanelPhysicalHeightMicroMeters / 1000f);
return ResultCode.Success;
}
[CommandCmif(1005)] // 21.0.0+
// GetPanelResolutionForSpecificTitle() -> (s32 width, s32 height)
public ResultCode GetPanelResolutionForSpecificTitle(ServiceCtx context)
{
if (!IsVrLaboGoggleSupportedTitle(context))
{
return ResultCode.ObjectInvalid;
}
context.ResponseData.Write(_resolutionWidth);
context.ResponseData.Write(_resolutionHeight);
return ResultCode.Success;
}
private static bool IsVrLaboGoggleSupportedTitle(ServiceCtx context)
{
ulong programId = context.Device.Processes.ActiveApplication.ProgramId;
return _supportedVRGoggleTitles.Contains(programId);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
if (isDisposing) if (isDisposing)
@@ -0,0 +1,80 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common;
using System;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy
{
class ILockAccessor : IpcService
{
private readonly ServiceCtx _context;
private readonly Apm.ManagerServer _apmManagerServer;
private readonly Apm.SystemManagerServer _apmSystemManagerServer;
private readonly KEvent _lockEvent;
private int _lockEventHandle;
public ILockAccessor(ServiceCtx context)
{
_context = context;
_apmManagerServer = new Apm.ManagerServer(context);
_apmSystemManagerServer = new Apm.SystemManagerServer(context);
_lockEvent = new KEvent(context.Device.System.KernelContext);
}
[CommandCmif(1)]
// TryLock(unknown u8 bool) -> unknown u8 bool
public ResultCode TryLock(ServiceCtx context)
{
// Official sw only uses inflag=false.
// Official sw just closes the output handle.
// The input flag controls whether this returns the output handle.
bool unknown = context.RequestData.ReadBoolean();
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { unknown });
context.ResponseData.Write(false);
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_lockEventHandle);
return ResultCode.Success;
}
[CommandCmif(2)]
// Unlock()
public ResultCode Unlock(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(3)]
// GetEvent() -> EventHandle w/ autoclear = false
public ResultCode GetEvent(ServiceCtx context)
{
if (_lockEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_lockEvent.ReadableEvent, out _lockEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_lockEventHandle);
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(4)] // 10.0.0+
// IsLocked() -> unknown u8 bool
public ResultCode IsLocked(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm);
context.ResponseData.Write(false);
return ResultCode.Success;
}
}
}
@@ -32,5 +32,6 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
DetectShortPressingCaptureButton = 90, DetectShortPressingCaptureButton = 90,
AlbumScreenShotTaken = 92, AlbumScreenShotTaken = 92,
AlbumRecordingSaved = 93, AlbumRecordingSaved = 93,
StartupLogoDisappeared = 95, // 21.0.0+
} }
} }
@@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
private readonly bool[] _supportedPlayers; private readonly bool[] _supportedPlayers;
private VibrationValue _neutralVibrationValue = new() private VibrationValue _neutralVibrationValue = new()
{ {
AmplitudeLow = 0f, AmplitudeLow = 0.01f,
FrequencyLow = 160f, FrequencyLow = 160f,
AmplitudeHigh = 0f, AmplitudeHigh = 0f,
FrequencyHigh = 320f, FrequencyHigh = 320f,
@@ -182,8 +182,10 @@ namespace Ryujinx.HLE.Loaders.Processes
ArmProcessContextFactory processContextFactory = new( ArmProcessContextFactory processContextFactory = new(
context.Device.System.TickSource, context.Device.System.TickSource,
context.Device.Gpu, context.Device.Gpu,
string.Empty, kip.ProgramId,
string.Empty, 0,
kip.Version.ToString(),
ProcessResult.GetProcessKind(kip.ProgramId),
false, false,
null, null,
codeAddress, codeAddress,
@@ -377,8 +379,10 @@ namespace Ryujinx.HLE.Loaders.Processes
ArmProcessContextFactory processContextFactory = new( ArmProcessContextFactory processContextFactory = new(
context.Device.System.TickSource, context.Device.System.TickSource,
context.Device.Gpu, context.Device.Gpu,
$"{programId:x16}", programId,
programIndex,
displayVersion, displayVersion,
ProcessResult.GetProcessKind(programId),
diskCacheEnabled, diskCacheEnabled,
diskCacheSelector, diskCacheSelector,
codeStart, codeStart,
@@ -87,7 +87,7 @@ namespace Ryujinx.HLE.Loaders.Processes
AllowCodeMemoryForJit = allowCodeMemoryForJit; AllowCodeMemoryForJit = allowCodeMemoryForJit;
} }
private static ProcessKind GetProcessKind(ulong programId) internal static ProcessKind GetProcessKind(ulong programId)
{ {
if (programId == 0) if (programId == 0)
{ {
+207 -70
View File
@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using SDL; using SDL;
using static SDL.SDL3; using static SDL.SDL3;
@@ -12,140 +13,276 @@ namespace Ryujinx.Input.SDL3
{ {
private readonly SDL_hid_device* _hidHandle; private readonly SDL_hid_device* _hidHandle;
private byte[] _buffer;
private static ushort _vendor;
private static ushort _product;
private int _globalCount; private int _globalCount;
private ulong _lastWriteTicks;
private NpadHdRumble(SDL_hid_device* hidHandle) private NpadHdRumble(SDL_hid_device* hidHandle)
{ {
_hidHandle = hidHandle; _hidHandle = hidHandle;
InitializeDevice();
} }
public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle) public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle)
{ {
ushort vendor = SDL_GetGamepadVendor(gamepadHandle); _vendor = SDL_GetGamepadVendor(gamepadHandle);
if (vendor != 0x057e) if (!Enum.IsDefined(typeof(HDRumbleSupportedVendor), _vendor))
{ {
return null; return null;
} }
ushort product = SDL_GetGamepadProduct(gamepadHandle); _product = SDL_GetGamepadProduct(gamepadHandle);
if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e) if (!Enum.IsDefined(typeof(HDRumbleSupportedProduct), _product))
{ {
return null; return null;
} }
return new NpadHdRumble(SDL_hid_open(vendor, product, 0)); int serialNumber = 0;
string? serial = SDL_GetGamepadSerial(gamepadHandle);
if (serial is not null)
{
int.TryParse(serial, out serialNumber);
}
return new NpadHdRumble(SDL_hid_open(_vendor, _product, serialNumber));
} }
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
private void WriteHdRumble( private bool WriteNintendoHdRumble(VibrationValue left, VibrationValue right)
int encLeftLowFreq, int encLeftLowAmp,
int encLeftHighFreq, int encLeftHighAmp,
int encRightLowFreq, int encRightLowAmp,
int encRightHighFreq, int encRightHighAmp)
{ {
byte[] buf = new byte[10]; int leftLowAmp = EncodeLowAmp(left.AmplitudeLow);
int leftLowFreq = EncodeLowFreq(left.FrequencyLow) + (leftLowAmp >> 8);
buf[0] = 0x10; int leftHighFreq = EncodeHighFreq(left.FrequencyHigh);
buf[1] = (byte)((++_globalCount) & 0xF); int leftHighAmp = EncodeHighAmp(left.AmplitudeHigh) + (leftHighFreq >> 8);
buf[2] = (byte)(encLeftHighFreq & 0xFF); int rightLowAmp = EncodeLowAmp(right.AmplitudeLow);
buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF)); int rightLowFreq = EncodeLowFreq(right.FrequencyLow) + (rightLowAmp >> 8);
buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF)); int rightHighFreq = EncodeHighFreq(right.FrequencyHigh);
buf[5] = (byte)(encLeftLowAmp & 0xFF); int rightHighAmp = EncodeHighAmp(right.AmplitudeHigh) + (rightHighFreq >> 8);
buf[6] = (byte)(encRightHighFreq & 0xFF); _buffer[0] = 0x10;
buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF)); _buffer[1] = (byte)((_globalCount++) & 0xF);
buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF));
buf[9] = (byte)(encRightLowAmp & 0xFF); // Left LRA
_buffer[2] = (byte)(leftLowFreq & 0xFF);
_buffer[3] = (byte)(leftHighAmp & 0xFF);
_buffer[4] = (byte)(leftHighFreq & 0xFF);
_buffer[5] = (byte)(leftLowAmp & 0xFF);
// Right LRA
_buffer[6] = (byte)(rightLowFreq & 0xFF);
_buffer[7] = (byte)(rightHighAmp & 0xFF);
_buffer[8] = (byte)(rightHighFreq & 0xFF);
_buffer[9] = (byte)(rightLowAmp & 0xFF);
if (_globalCount > 0xF) if (_globalCount > 0xF)
{ {
_globalCount = 0x0; _globalCount = 0x0;
} }
fixed (byte* ptr = buf) fixed (byte* ptr = _buffer)
{ {
SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length); if (SendHdRumble(ptr, (nuint)_buffer.Length) >= 0)
{
return true;
}
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
} }
return false;
} }
private static int EncodeLowFreq(float lowFreq) private static int EncodeLowFreq(float lowFreq)
{ {
float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); return (int)Math.Clamp(32 * Math.Log2(lowFreq * 0.1f) - 0x40, 81.75177f, 1252.572266f);
return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40;
} }
private static int EncodeHighFreq(float highFreq) private static int EncodeHighFreq(float highFreq)
{ {
float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); return (int)Math.Clamp(32 * Math.Log2(highFreq * 0.1f) - 0x60, 81.75177f, 1252.572266f);
return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4;
} }
private static int EncodeLowAmp(float rawAmp) private static int EncodeLowAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1; encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f) else if (rawAmp is >= 0.012f and < 0.112f)
{ encodedAmp = 4 * Math.Log2(rawAmp * 110f);
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f) else if (rawAmp is >= 0.112f and < 0.225f)
{ encodedAmp = 16 * Math.Log2(rawAmp * 17f);
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f) else if (rawAmp is >= 0.225f and <= 1f)
{ encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
} encodedAmp = Math.Round((encodedAmp / 2.0) + 64.0);
encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867);
return (int)Math.Floor(encodedAmp / 2.0) + 64; return (int)Math.Round(encodedAmp);
} }
private static int EncodeHighAmp(float rawAmp) private static int EncodeHighAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1; encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f) else if (rawAmp is >= 0.012f and < 0.112f)
{ encodedAmp = 4 * Math.Log2(rawAmp * 110f);
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f) else if (rawAmp is >= 0.112f and < 0.225f)
{ encodedAmp = 16 * Math.Log2(rawAmp * 17f);
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f) else if (rawAmp is >= 0.225f and <= 1f)
{ encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
} encodedAmp = Math.Round(encodedAmp / 2.0);
encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867);
return encodedAmp * 2; return (int)encodedAmp;
} }
public bool HdRumble(VibrationValue left, VibrationValue right) public bool HdRumble(VibrationValue left, VibrationValue right)
{ {
WriteHdRumble(EncodeLowFreq(left.FrequencyLow), if(_product is (ushort) HDRumbleSupportedProduct.ProController
EncodeLowAmp(left.AmplitudeLow), or (ushort) HDRumbleSupportedProduct.JoyconLeft
EncodeHighFreq(left.FrequencyHigh), or (ushort) HDRumbleSupportedProduct.JoyconRight
EncodeHighAmp(left.AmplitudeHigh), or (ushort) HDRumbleSupportedProduct.JoyconPair
EncodeLowFreq(right.FrequencyLow), or (ushort) HDRumbleSupportedProduct.JoyconGrip)
EncodeLowAmp(right.AmplitudeLow), {
EncodeHighFreq(right.FrequencyHigh), return WriteNintendoHdRumble(left, right);
EncodeHighAmp(right.AmplitudeHigh)); }
return true;
return false;
}
private int SendHdRumble(byte* data, nuint length)
{
int result = 0;
ulong currentTicks = SDL_GetTicks();
// Ditch rumble if we haven't hit the poll-rate yet.
if ((currentTicks - _lastWriteTicks) <= GetPollRate())
{
return result;
}
result = SDL_hid_write(_hidHandle, data, length);
if (result >= 0)
{
_lastWriteTicks = currentTicks;
}
return result;
}
private void InitializeDevice()
{
if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo)
{
_buffer = new byte[10];
byte[] init = new byte[64];
// Pro Controller and Charge Grip
if (_product
is (ushort)HDRumbleSupportedProduct.ProController
or (ushort)HDRumbleSupportedProduct.JoyconGrip)
{
SDL_LockJoysticks();
fixed (byte* ptr = init)
{
init[0] = 0x80;
init[1] = 0x05; // Allow bluetooth timeout TODO: use 0x04 to force USB only (toggle?)
SDL_hid_write(_hidHandle, ptr, 64);
}
SDL_UnlockJoysticks();
return;
}
// Joycons
if (_product
is (ushort)HDRumbleSupportedProduct.JoyconLeft
or (ushort)HDRumbleSupportedProduct.JoyconRight
or (ushort)HDRumbleSupportedProduct.JoyconPair)
{
SDL_LockJoysticks();
fixed (byte* ptr = init)
{
// we could write data to the controller here (see above)
}
SDL_UnlockJoysticks();
return;
}
}
}
private ulong GetPollRate()
{
ulong pollRate = 0;
if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo)
{
// https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX
pollRate = (ulong) 16.67;
if (_product is (ushort)HDRumbleSupportedProduct.ProController
&& SDL_hid_get_device_info(_hidHandle)->bus_type == SDL_hid_bus_type.SDL_HID_API_BUS_USB)
{
pollRate = (ulong) 8.33;
}
}
return pollRate;
} }
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this);
SDL_hid_close(_hidHandle); SDL_hid_close(_hidHandle);
} }
} }
public enum HDRumbleSupportedVendor : ushort
{
Nintendo = 0x057e,
Valve = 0x28de,
Sony = 0x054c
}
public enum HDRumbleSupportedProduct : ushort
{
// TODO: Currently, HD Rumble only supports the Pro Controller and JoyCons.
// We need to initialize and report to each device differently.
// Nintendo Switch: 0x057e
JoyconLeft = 0x2006,
JoyconRight = 0x2007,
JoyconPair = 0x2008,
ProController = 0x2009,
JoyconGrip = 0x200e,
// Nintendo Switch 2: 0x057e
Joycon2Right = 0x2066,
Joycon2Left = 0x2067,
Joycon2Pair = 0x2068,
Switch2ProController = 0x2069,
GamecubeController = 0x2073,
// Valve Steam Family: 0x28de
// https://github.com/libsdl-org/SDL/issues/9148
SteamDeck = 0x11ff,
SteamDeckVirtualDevice = 0x1205,
SteamController = 0x1106,
// PlayStation Dualsense: 0x054c
Dualsense = 0x0ce6
}
} }
+13 -2
View File
@@ -177,10 +177,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; return _hdRumble?.HdRumble(left, right) ?? false;
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
if ((Features & GamepadFeaturesFlag.Rumble) == 0) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -199,6 +201,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
} }
if (!String.IsNullOrEmpty(SDL_GetError()))
{
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
return false;
}
return true;
} }
public Vector3 GetMotionData(MotionInputId inputId) public Vector3 GetMotionData(MotionInputId inputId)
+14 -3
View File
@@ -163,7 +163,7 @@ namespace Ryujinx.Input.SDL3
public void SetTriggerThreshold(float triggerThreshold) public void SetTriggerThreshold(float triggerThreshold)
{ {
// No operations
} }
public bool HDRumble(VibrationValue left, VibrationValue right) public bool HDRumble(VibrationValue left, VibrationValue right)
@@ -171,10 +171,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; return _hdRumble?.HdRumble(left, right) ?? false;
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
if ((Features & GamepadFeaturesFlag.Rumble) == 0) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -193,6 +195,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
} }
if (!String.IsNullOrEmpty(SDL_GetError()))
{
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
return false;
}
return true;
} }
public Vector3 GetMotionData(MotionInputId inputId) public Vector3 GetMotionData(MotionInputId inputId)
+21 -2
View File
@@ -1,4 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Gommon;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3
return left.IsPressed(inputId) || right.IsPressed(inputId); return left.IsPressed(inputId) || right.IsPressed(inputId);
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool HDRumble(VibrationValue left, VibrationValue right)
{
// return _hdRumble?.HdRumble(left, right) ?? false;
// TODO: Track rumble and motion on both controllers
return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
if (lowFrequency != 0) if (lowFrequency != 0)
{ {
@@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3
left.Rumble(0, 0, durationMs); left.Rumble(0, 0, durationMs);
right.Rumble(0, 0, durationMs); right.Rumble(0, 0, durationMs);
} }
if (!SDL_GetError().IsNullOrEmpty())
{
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
return false;
}
return true;
} }
public void SetConfiguration(InputConfig configuration) public void SetConfiguration(InputConfig configuration)
+8 -2
View File
@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using SDL; using SDL;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
@@ -385,9 +386,14 @@ namespace Ryujinx.Input.SDL3
// No operations // No operations
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool HDRumble(VibrationValue left, VibrationValue right)
{ {
// No operations return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
return false;
} }
public Vector3 GetMotionData(MotionInputId inputId) public Vector3 GetMotionData(MotionInputId inputId)
+7 -1
View File
@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Drawing; using System.Drawing;
using System.Numerics; using System.Numerics;
@@ -66,7 +67,12 @@ namespace Ryujinx.Input.SDL3
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool HDRumble(VibrationValue left, VibrationValue right)
{
throw new NotImplementedException();
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
+30 -26
View File
@@ -554,34 +554,38 @@ namespace Ryujinx.Input.HLE
{ {
if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue)) if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue))
{ {
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble) if (_config is not StandardControllerInputConfig controllerConfig ||
!controllerConfig.Rumble.EnableRumble)
{ {
VibrationValue leftVibrationValue = dualVibrationValue.Item1; return;
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
{
_gamepad.Rumble(low, high, uint.MaxValue);
}
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
$"L.high.amp={leftVibrationValue.AmplitudeHigh}, " +
$"L.low.freq={leftVibrationValue.FrequencyLow}, " +
$"L.high.freq={leftVibrationValue.FrequencyHigh}, " +
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
$"R.low.freq={rightVibrationValue.FrequencyLow}, " +
$"R.high.freq={rightVibrationValue.FrequencyHigh}");
} }
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15)));
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85)));
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
if (!controllerConfig.Rumble.UseHDRumble || !_gamepad.HDRumble(leftVibrationValue, rightVibrationValue))
{
_gamepad.Rumble(low, high, 0xFFFFFFFF);
}
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
// Value=value/multiplier * multiplier (result)
$"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " +
$"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({leftVibrationValue.AmplitudeHigh}), " +
$"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " +
$"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({leftVibrationValue.FrequencyHigh}), " +
$"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({rightVibrationValue.AmplitudeLow}), " +
$"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " +
$"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({rightVibrationValue.FrequencyLow}), " +
$"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})");
} }
} }
} }
+3 -6
View File
@@ -71,10 +71,7 @@ namespace Ryujinx.Input
/// </summary> /// </summary>
/// <param name="left">The vibration data for the left side</param> /// <param name="left">The vibration data for the left side</param>
/// <param name="right">The vibration data for the right side</param> /// <param name="right">The vibration data for the right side</param>
bool HDRumble(VibrationValue left, VibrationValue right) bool HDRumble(VibrationValue left, VibrationValue right);
{
return false;
}
/// <summary> /// <summary>
/// Starts a rumble effect on the gamepad. /// Starts a rumble effect on the gamepad.
@@ -82,10 +79,10 @@ namespace Ryujinx.Input
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param> /// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param> /// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
/// <param name="durationMs">The duration of the rumble effect in milliseconds.</param> /// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
void Rumble(float lowFrequency, float highFrequency, uint durationMs); bool Rumble(float lowFrequency, float highFrequency, uint durationMs);
/// <summary> /// <summary>
/// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>. /// Get a snaphost of the state of the gamepad that is remapped with the information from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
/// </summary> /// </summary>
/// <returns>A remapped snaphost of the state of the gamepad.</returns> /// <returns>A remapped snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetMappedStateSnapshot(); GamepadStateSnapshot GetMappedStateSnapshot();
@@ -1422,6 +1422,7 @@ namespace Ryujinx.UI.Common.Configuration
EnableRumble = false, EnableRumble = false,
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
UseHDRumble = false
}; };
} }
} }
@@ -15,6 +15,9 @@ namespace Ryujinx.UI.Common.Helper
public static string OverrideBackendThreading { get; private set; } public static string OverrideBackendThreading { get; private set; }
public static string OverrideHideCursor { get; private set; } public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { get; private set; } public static string BaseDirPathArg { get; private set; }
public static string RenderDocCaptureTitleFormat { get; private set; } =
"{EmuVersion}\n{GuestName} {GuestVersion} {GuestTitleId} {GuestArch}";
public static FilePath FirmwareToInstallPathArg { get; set; } public static FilePath FirmwareToInstallPathArg { get; set; }
public static string Profile { get; private set; } public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; } public static string LaunchPathArg { get; private set; }
@@ -45,6 +48,20 @@ namespace Ryujinx.UI.Common.Helper
BaseDirPathArg = args[++i]; BaseDirPathArg = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "-rdct":
case "--rd-capture-title-format":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
RenderDocCaptureTitleFormat = args[++i];
arguments.Add(arg); arguments.Add(arg);
arguments.Add(args[i]); arguments.Add(args[i]);
break; break;
@@ -1,3 +1,4 @@
using Gommon;
using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.Loaders.Processes;
using System; using System;
@@ -26,5 +27,23 @@ namespace Ryujinx.UI.Common.Helper
return appTitle; return appTitle;
} }
public static string FormatRenderDocCaptureTitle(ProcessResult activeProcess, string applicationVersion)
{
if (activeProcess == null)
return string.Empty;
string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : activeProcess.Name;
string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $"v{activeProcess.DisplayVersion}";
string titleIdSection = $"({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? "(64-bit)" : "(32-bit)";
return CommandLineState.RenderDocCaptureTitleFormat
.ReplaceIgnoreCase("{EmuVersion}", applicationVersion)
.ReplaceIgnoreCase("{GuestName}", titleNameSection)
.ReplaceIgnoreCase("{GuestVersion}", titleVersionSection)
.ReplaceIgnoreCase("{GuestTitleId}", titleIdSection)
.ReplaceIgnoreCase("{GuestArch}", titleArchSection);
}
} }
} }
+1
View File
@@ -14,6 +14,7 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Helper;
using System; using System;
+20 -4
View File
@@ -45,6 +45,7 @@ using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Helper;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using SkiaSharp; using SkiaSharp;
using SPB.Graphics.Exceptions;
using SPB.Graphics.Vulkan; using SPB.Graphics.Vulkan;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -654,8 +655,22 @@ namespace Ryujinx.Ava
if (RendererHost.EmbeddedWindow is EmbeddedWindowOpenGL openGlWindow) if (RendererHost.EmbeddedWindow is EmbeddedWindowOpenGL openGlWindow)
{ {
// Try to bind the OpenGL context before calling the shutdown event. try
openGlWindow.MakeCurrent(false, false); {
// Try to bind the OpenGL context before disposing GPU resources.
openGlWindow.MakeCurrent();
}
catch (ContextException e) when (_userChannelPersistence.ShouldRestart)
{
// ExecuteProgram may detach the old native window before GPU
// disposal. Allow the old context to be released with the window
// and continue the requested program relaunch.
Logger.Warning?.Print(
LogClass.UI,
$"Failed to bind OpenGL context during program relaunch: {e}");
return;
}
Device.DisposeGpu(); Device.DisposeGpu();
@@ -1075,9 +1090,10 @@ namespace Ryujinx.Ava
{ {
Dispatcher.UIThread.InvokeAsync(() => Dispatcher.UIThread.InvokeAsync(() =>
{ {
if (_viewModel.StartGamesInFullscreen) if (_viewModel.StartGamesInFullscreen && _viewModel.WindowState is not WindowState.FullScreen)
{ {
_viewModel.WindowState = WindowState.FullScreen; // Use the view model toggle so decoration ordering matches user toggles.
_viewModel.ToggleFullscreen();
} }
if (_viewModel.WindowState == WindowState.FullScreen || _viewModel.StartGamesWithoutUI) if (_viewModel.WindowState == WindowState.FullScreen || _viewModel.StartGamesWithoutUI)
+7 -1
View File
@@ -462,6 +462,8 @@
"ControllerSettingsRumble": "Rumble", "ControllerSettingsRumble": "Rumble",
"ControllerSettingsRumbleStrongMultiplier": "Strong Rumble Multiplier", "ControllerSettingsRumbleStrongMultiplier": "Strong Rumble Multiplier",
"ControllerSettingsRumbleWeakMultiplier": "Weak Rumble Multiplier", "ControllerSettingsRumbleWeakMultiplier": "Weak Rumble Multiplier",
"ControllerSettingsRumbleUseHDRumble": "Enable HD Rumble",
"HDRumbleTooltip": "EXPERIMENTAL.\n\nSends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave OFF if unsure.",
"DialogMessageSaveNotAvailableMessage": "There is no savedata for {0} [{1:x16}]", "DialogMessageSaveNotAvailableMessage": "There is no savedata for {0} [{1:x16}]",
"DialogMessageSaveNotAvailableCreateSaveMessage": "Would you like to create savedata for this game?", "DialogMessageSaveNotAvailableCreateSaveMessage": "Would you like to create savedata for this game?",
"DialogConfirmationTitle": "Ryujinx - Confirmation", "DialogConfirmationTitle": "Ryujinx - Confirmation",
@@ -932,5 +934,9 @@
"GameListContextMenuExtractDataAocRomFSToolTip": "Extract the RomFS from a selected DLC file", "GameListContextMenuExtractDataAocRomFSToolTip": "Extract the RomFS from a selected DLC file",
"ExtractAocListHeader": "Select a DLC to Extract", "ExtractAocListHeader": "Select a DLC to Extract",
"SettingsTabSystemSkipUserProfilesManager": "Skip Dialog 'Manage User Profiles'", "SettingsTabSystemSkipUserProfilesManager": "Skip Dialog 'Manage User Profiles'",
"SkipUserProfilesTooltip": "This option skips the 'Manage User Profiles' dialog during gameplay, using a pre-selected profile.\n\nProfile switching is found in 'Settings' - 'Manager User Profiles'. Select the desired profile before loading the game." "SkipUserProfilesTooltip": "This option skips the 'Manage User Profiles' dialog during gameplay, using a pre-selected profile.\n\nProfile switching is found in 'Settings' - 'Manager User Profiles'. Select the desired profile before loading the game.",
"MenuBarActions_StartCapture": "Start RenderDoc Frame Capture",
"MenuBarActions_EndCapture": "End RenderDoc Frame Capture",
"MenuBarActions_DiscardCapture": "Discard RenderDoc Frame Capture",
"MenuBarActions_DiscardCapture_ToolTip": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result."
} }
+5 -1
View File
@@ -807,5 +807,9 @@
"MultiplayerModeDisabled": "Deshabilitar", "MultiplayerModeDisabled": "Deshabilitar",
"MultiplayerModeLdnMitm": "ldn_mitm", "MultiplayerModeLdnMitm": "ldn_mitm",
"SettingsTabSystemSkipUserProfilesManager": "Omitir el Diálogo 'Gestionar Perfiles de Usuario'", "SettingsTabSystemSkipUserProfilesManager": "Omitir el Diálogo 'Gestionar Perfiles de Usuario'",
"SkipUserProfilesTooltip": "Esta opción omite el diálogo de 'Gestionar perfiles de usuario' durante el juego, utilizando un perfil preseleccionado.\n\nEl cambio de perfil se encuentra en 'Configuración' - 'Gestionar perfiles de usuario'. Seleccione el perfil deseado antes de cargar el juego." "SkipUserProfilesTooltip": "Esta opción omite el diálogo de 'Gestionar perfiles de usuario' durante el juego, utilizando un perfil preseleccionado.\n\nEl cambio de perfil se encuentra en 'Configuración' - 'Gestionar perfiles de usuario'. Seleccione el perfil deseado antes de cargar el juego.",
"MenuBarActions_StartCapture": "Iniciar una captura de fotograma de RenderDoc",
"MenuBarActions_EndCapture": "Detener la captura de fotograma de RenderDoc",
"MenuBarActions_DiscardCapture": "Descartar la captura de fotograma de RenderDoc",
"MenuBarActions_DiscardCapture_ToolTip": "Finaliza la captura de fotograma de RenderDoc actualmente activa y descarta inmediatamente su resultado."
} }
+5 -1
View File
@@ -828,5 +828,9 @@
"GameListContextMenuExtractDataAocRomFSToolTip": "Extraire les RomFS d'un fichier DLC choisi", "GameListContextMenuExtractDataAocRomFSToolTip": "Extraire les RomFS d'un fichier DLC choisi",
"ExtractAocListHeader": "Choisissez un DLC à extraire", "ExtractAocListHeader": "Choisissez un DLC à extraire",
"SettingsTabSystemSkipUserProfilesManager": "Ignorer la Boîte de Dialogue « Gérer les Profils d'Utilisateurs »", "SettingsTabSystemSkipUserProfilesManager": "Ignorer la Boîte de Dialogue « Gérer les Profils d'Utilisateurs »",
"SkipUserProfilesTooltip": "Cette option permet d'éviter le dialogue du 'Gérer les profils d'utilisateurs' pendant le jeu, en utilisant un profil pré-sélectionné.\n\nLa sélection du profil se trouve dans 'Paramètres' - 'Gérer les profils d'utilisateurs'. Sélectionnez le profil souhaité avant de charger la partie." "SkipUserProfilesTooltip": "Cette option permet d'éviter le dialogue du 'Gérer les profils d'utilisateurs' pendant le jeu, en utilisant un profil pré-sélectionné.\n\nLa sélection du profil se trouve dans 'Paramètres' - 'Gérer les profils d'utilisateurs'. Sélectionnez le profil souhaité avant de charger la partie.",
"MenuBarActions_StartCapture": "Démarrer une capture de trame RenderDoc",
"MenuBarActions_EndCapture": "Arrêter la capture de trame RenderDoc",
"MenuBarActions_DiscardCapture": "Supprimer la capture de trame RenderDoc",
"MenuBarActions_DiscardCapture_ToolTip": "Met fin à la capture de trame RenderDoc en cours, en supprimant immédiatement son résultat."
} }
@@ -223,6 +223,7 @@ namespace Ryujinx.Headless
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = true
}, },
}; };
} }
+14 -2
View File
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -149,9 +150,20 @@ namespace Ryujinx.Ava.Input
} }
} }
public void SetTriggerThreshold(float triggerThreshold) { } public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { } public bool HDRumble(VibrationValue left, VibrationValue right)
{
return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
return false;
}
public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero; public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;
+7 -1
View File
@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Drawing; using System.Drawing;
@@ -63,8 +64,13 @@ namespace Ryujinx.Ava.Input
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public bool HDRumble(VibrationValue left, VibrationValue right)
{
throw new NotImplementedException();
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
+1
View File
@@ -8,6 +8,7 @@ using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.Graphics.Vulkan.MoltenVK; using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless; using Ryujinx.Headless;
using Ryujinx.Modules; using Ryujinx.Modules;
+1
View File
@@ -56,6 +56,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" /> <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
@@ -19,6 +19,7 @@ namespace Ryujinx.Ava.UI.Models.Input
public float WeakRumble { get; set; } public float WeakRumble { get; set; }
public float StrongRumble { get; set; } public float StrongRumble { get; set; }
public bool UseHDRumble { get; set; }
public string Id { get; set; } public string Id { get; set; }
public ControllerType ControllerType { get; set; } public ControllerType ControllerType { get; set; }
@@ -202,6 +203,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = controllerInput.Rumble.EnableRumble; EnableRumble = controllerInput.Rumble.EnableRumble;
WeakRumble = controllerInput.Rumble.WeakRumble; WeakRumble = controllerInput.Rumble.WeakRumble;
StrongRumble = controllerInput.Rumble.StrongRumble; StrongRumble = controllerInput.Rumble.StrongRumble;
UseHDRumble = controllerInput.Rumble.UseHDRumble;
} }
} }
} }
@@ -259,6 +261,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = EnableRumble, EnableRumble = EnableRumble,
WeakRumble = WeakRumble, WeakRumble = WeakRumble,
StrongRumble = StrongRumble, StrongRumble = StrongRumble,
UseHDRumble = UseHDRumble,
}, },
Version = InputConfig.CurrentVersion, Version = InputConfig.CurrentVersion,
DeadzoneLeft = DeadzoneLeft, DeadzoneLeft = DeadzoneLeft,
+59 -3
View File
@@ -4,6 +4,9 @@ using Avalonia.Platform;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper; using Ryujinx.UI.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE;
using SPB.Graphics; using SPB.Graphics;
using SPB.Platform; using SPB.Platform;
using SPB.Platform.GLX; using SPB.Platform.GLX;
@@ -30,6 +33,7 @@ namespace Ryujinx.Ava.UI.Renderer
protected nint MetalLayer { get; set; } protected nint MetalLayer { get; set; }
public delegate void UpdateBoundsCallbackDelegate(Rect rect); public delegate void UpdateBoundsCallbackDelegate(Rect rect);
private UpdateBoundsCallbackDelegate _updateBoundsCallback; private UpdateBoundsCallbackDelegate _updateBoundsCallback;
public event EventHandler<nint> WindowCreated; public event EventHandler<nint> WindowCreated;
@@ -46,6 +50,55 @@ namespace Ryujinx.Ava.UI.Renderer
protected virtual void OnWindowDestroyed() { } protected virtual void OnWindowDestroyed() { }
public bool ToggleRenderDocCapture(Switch device)
{
if (!RenderDoc.IsAvailable) return false;
if (RenderDoc.IsFrameCapturing)
{
if (EndRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
return true;
}
}
else if (StartRenderDocCapture(device))
{
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
return true;
}
return false;
}
public bool StartRenderDocCapture(Switch device)
{
if (!RenderDoc.IsAvailable) return false;
if (RenderDoc.IsFrameCapturing) return false;
RenderDoc.StartFrameCapture(nint.Zero, WindowHandle);
RenderDoc.SetCaptureTitle(TitleHelper.FormatRenderDocCaptureTitle(device.Processes.ActiveApplication, Program.Version));
return true;
}
public bool EndRenderDocCapture()
{
if (!RenderDoc.IsAvailable) return false;
if (!RenderDoc.IsFrameCapturing) return false;
return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle);
}
public bool DiscardRenderDocCapture()
{
if (!RenderDoc.IsAvailable) return false;
if (!RenderDoc.IsFrameCapturing) return false;
return RenderDoc.IsFrameCapturing && RenderDoc.DiscardFrameCapture(nint.Zero, WindowHandle);
}
protected virtual void OnWindowDestroying() protected virtual void OnWindowDestroying()
{ {
WindowHandle = nint.Zero; WindowHandle = nint.Zero;
@@ -124,7 +177,9 @@ namespace Ryujinx.Ava.UI.Renderer
} }
else else
{ {
X11Window = PlatformHelper.CreateOpenGLWindow(new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, 100) as GLXWindow; X11Window = PlatformHelper.CreateOpenGLWindow(
new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100,
100) as GLXWindow;
} }
if (X11Window != null) if (X11Window != null)
@@ -141,7 +196,7 @@ namespace Ryujinx.Ava.UI.Renderer
{ {
_className = "NativeWindow-" + Guid.NewGuid(); _className = "NativeWindow-" + Guid.NewGuid();
_wndProcDelegate = delegate (nint hWnd, WindowsMessages msg, nint wParam, nint lParam) _wndProcDelegate = delegate(nint hWnd, WindowsMessages msg, nint wParam, nint lParam)
{ {
switch (msg) switch (msg)
{ {
@@ -164,7 +219,8 @@ namespace Ryujinx.Ava.UI.Renderer
RegisterClassEx(ref wndClassEx); RegisterClassEx(ref wndClassEx);
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, control.Handle, nint.Zero, nint.Zero, nint.Zero); WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480,
control.Handle, nint.Zero, nint.Zero, nint.Zero);
SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc); SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc);
@@ -678,6 +678,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = false
}, },
}; };
} }
@@ -5,9 +5,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public partial class RumbleInputViewModel : BaseModel public partial class RumbleInputViewModel : BaseModel
{ {
[ObservableProperty] [ObservableProperty]
private float _strongRumble; public partial float StrongRumble { get; set; }
[ObservableProperty] [ObservableProperty]
private float _weakRumble; public partial float WeakRumble { get; set; }
[ObservableProperty]
public partial bool EnableHDRumble { get; set; }
} }
} }
@@ -7,6 +7,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Threading; using Avalonia.Threading;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@@ -25,6 +26,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE; using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
@@ -1833,6 +1835,31 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
public void ReloadRenderDocApi()
{
RenderDoc.ReloadApi(ignoreAlreadyLoaded: true);
OnPropertyChanged(nameof(ShowStartCaptureButton));
OnPropertyChanged(nameof(ShowEndCaptureButton));
OnPropertyChanged(nameof(RenderDocIsAvailable));
if (RenderDoc.IsAvailable)
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
NotificationHelper.ShowInformation(
"RenderDoc API reloaded",
RenderDoc.IsAvailable ? "RenderDoc is now available." : "RenderDoc is no longer available."
);
}
public void ToggleCapture()
{
if (ShowLoadProgress) return;
AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device);
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public void ToggleFullscreen() public void ToggleFullscreen()
{ {
if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs) if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs)
@@ -2112,5 +2139,25 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
#endregion #endregion
#region Context Menu commands
public bool ShowStartCaptureButton => !RenderDocIsCapturing && RenderDoc.IsAvailable;
public bool ShowEndCaptureButton => RenderDocIsCapturing && RenderDoc.IsAvailable;
public static bool RenderDocIsAvailable => RenderDoc.IsAvailable;
public bool RenderDocIsCapturing
{
get;
set
{
field = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowStartCaptureButton));
OnPropertyChanged(nameof(ShowEndCaptureButton));
}
}
#endregion
} }
} }
@@ -55,6 +55,15 @@
Margin="5,0" Margin="5,0"
Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" />
</StackPanel> </StackPanel>
<CheckBox
Margin="5"
IsChecked="{Binding EnableHDRumble}">
<TextBlock
Margin="0,3,0,0"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsRumbleUseHDRumble}"
ToolTip.Tip="{locale:Locale HDRumbleTooltip}" />
</CheckBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -24,6 +24,7 @@ namespace Ryujinx.Ava.UI.Views.Input
{ {
StrongRumble = config.StrongRumble, StrongRumble = config.StrongRumble,
WeakRumble = config.WeakRumble, WeakRumble = config.WeakRumble,
EnableHDRumble = config.UseHDRumble
}; };
InitializeComponent(); InitializeComponent();
@@ -49,6 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Input
GamepadInputConfig config = viewModel.Config; GamepadInputConfig config = viewModel.Config;
config.StrongRumble = content._viewModel.StrongRumble; config.StrongRumble = content._viewModel.StrongRumble;
config.WeakRumble = content._viewModel.WeakRumble; config.WeakRumble = content._viewModel.WeakRumble;
config.UseHDRumble = content._viewModel.EnableHDRumble;
}; };
await contentDialog.ShowAsync(); await contentDialog.ShowAsync();
@@ -189,13 +189,13 @@
Click="StopEmulation_Click" Click="StopEmulation_Click"
Header="{locale:Locale MenuBarOptionsStopEmulation}" Header="{locale:Locale MenuBarOptionsStopEmulation}"
InputGesture="Escape" InputGesture="Escape"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Name="RestartEmulationMenuItem"
Header="{locale:Locale MenuBarOptionsRestartEmulation}"
InputGesture="Ctrl + R"
IsEnabled="{Binding IsGameRunning}" IsEnabled="{Binding IsGameRunning}"
ToolTip.Tip="{locale:Locale StopEmulationTooltip}" /> ToolTip.Tip="{locale:Locale StopEmulationTooltip}" />
<MenuItem
Click="RestartEmulation_Click"
Header="{locale:Locale MenuBarOptionsRestartEmulation}"
InputGesture="Ctrl + R"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" /> <MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" />
<Separator /> <Separator />
<MenuItem <MenuItem
@@ -227,6 +227,26 @@
Click="OpenCheatManagerForCurrentApp" Click="OpenCheatManagerForCurrentApp"
Header="{locale:Locale GameListContextMenuManageCheat}" Header="{locale:Locale GameListContextMenuManageCheat}"
IsEnabled="{Binding IsGameRunning}" /> IsEnabled="{Binding IsGameRunning}" />
<Separator IsVisible="{Binding RenderDocIsAvailable}" />
<MenuItem
Click="StartRenderDocCapture_Click"
IsVisible="{Binding ShowStartCaptureButton}"
CommandParameter="{Binding}"
Header="{locale:Locale MenuBarActions_StartCapture}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Click="EndRenderDocCapture_Click"
IsVisible="{Binding ShowEndCaptureButton}"
CommandParameter="{Binding}"
Header="{locale:Locale MenuBarActions_EndCapture}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Click="DiscardRenderDocCapture_Click"
IsVisible="{Binding ShowEndCaptureButton}"
CommandParameter="{Binding}"
Header="{locale:Locale MenuBarActions_DiscardCapture}"
ToolTip.Tip="{locale:Locale MenuBarActions_DiscardCapture_ToolTip}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem> </MenuItem>
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}"> <MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}">
<MenuItem Header="{locale:Locale MenuBarToolsInstallKeys}" IsEnabled="{Binding EnableNonGameRunningControls}"> <MenuItem Header="{locale:Locale MenuBarToolsInstallKeys}" IsEnabled="{Binding EnableNonGameRunningControls}">
@@ -9,7 +9,9 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption; using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.UI.App.Common; using Ryujinx.UI.App.Common;
@@ -119,6 +121,11 @@ namespace Ryujinx.Ava.UI.Views.Main
Window.ViewModel.AppHost?.Resume(); Window.ViewModel.AppHost?.Resume();
} }
private void RestartEmulation_Click(object sender, RoutedEventArgs e)
{
Window.ViewModel?.RestartEmulation();
}
public async void OpenMiiApplet(object sender, RoutedEventArgs e) public async void OpenMiiApplet(object sender, RoutedEventArgs e)
{ {
string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
@@ -261,6 +268,52 @@ namespace Ryujinx.Ava.UI.Views.Main
} }
} }
public void StartRenderDocCapture_Click(object sender, RoutedEventArgs args)
{
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (!RenderDoc.IsFrameCapturing && RenderDoc.IsAvailable && viewModel is not { ShowLoadProgress: true })
{
if (viewModel != null && viewModel.AppHost.RendererHost
.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device))
{
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
}
}
viewModel?.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public void EndRenderDocCapture_Click(object sender, RoutedEventArgs args)
{
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (RenderDoc.IsFrameCapturing && RenderDoc.IsAvailable && viewModel is not { ShowLoadProgress: true })
{
if (viewModel != null && viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
}
}
viewModel?.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public void DiscardRenderDocCapture_Click(object sender, RoutedEventArgs args)
{
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (RenderDoc.IsFrameCapturing && RenderDoc.IsAvailable && viewModel is not { ShowLoadProgress: true })
{
if (viewModel != null && viewModel.AppHost.RendererHost.EmbeddedWindow.DiscardRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Discarded RenderDoc capture.");
}
}
viewModel?.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public async void CheckForUpdates(object sender, RoutedEventArgs e) public async void CheckForUpdates(object sender, RoutedEventArgs e)
{ {
if (Updater.CanUpdate(true)) if (Updater.CanUpdate(true))
+2
View File
@@ -43,6 +43,8 @@
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" /> <KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
<KeyBinding Gesture="Ctrl+," Command="{Binding OpenSettings}" /> <KeyBinding Gesture="Ctrl+," Command="{Binding OpenSettings}" />
<KeyBinding Gesture="Ctrl+R" Command="{Binding RestartEmulation}" /> <KeyBinding Gesture="Ctrl+R" Command="{Binding RestartEmulation}" />
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
</Window.KeyBindings> </Window.KeyBindings>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -6,6 +6,8 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.UI.Common.Models; using Ryujinx.UI.Common.Models;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Windows
@@ -81,20 +83,17 @@ namespace Ryujinx.Ava.UI.Windows
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
foreach (object content in e.AddedItems) List<XCITrimmerFileModel> added = [.. e.AddedItems.OfType<XCITrimmerFileModel>()];
List<XCITrimmerFileModel> removed = [.. e.RemovedItems.OfType<XCITrimmerFileModel>()];
foreach (XCITrimmerFileModel applicationData in added)
{ {
if (content is XCITrimmerFileModel applicationData) ViewModel.Select(applicationData);
{
ViewModel.Select(applicationData);
}
} }
foreach (object content in e.RemovedItems) foreach (XCITrimmerFileModel applicationData in removed)
{ {
if (content is XCITrimmerFileModel applicationData) ViewModel.Deselect(applicationData);
{
ViewModel.Deselect(applicationData);
}
} }
} }
} }