Compare commits

..

37 Commits

Author SHA1 Message Date
4741a05df9 Vulkan: Include DepthMode in ProgramPipelineState (#5185) 2023-06-01 09:05:39 +02:00
c6676007bf GPU: Dispose Renderer after running deferred actions (#5144)
* GAL: Dispose Renderer after running deferred actions

Deferred actions from disposing physical memory instances always dispose the resources in their caches. The renderer can't be disposed before these resources get disposed, otherwise the dispose actions will not actually run, and the ThreadedRenderer may get stuck trying to enqueue too many commands when there is nothing consuming them.

This should fix most instances of the emulator freezing on close.

* Wait for main render commands to finish, but keep RenderThread alive til dispose

* Address some feedback.

* No parameterize needed

* Set thread name as part of constructor

* Port to Ava and SDL2
2023-05-31 21:43:20 +00:00
92b0b7d753 Avalonia UI: Fix letter "x" in Ryujinx logo being cut off (#5176)
Also make the pronunciation center-aligned
2023-05-31 21:03:11 +00:00
232237bf28 Skip draws with zero vertex count (#5149) 2023-05-31 17:51:11 -03:00
c27e453fd3 Share ResourceManager vertex vertex A and B shaders (#5181) 2023-05-31 17:17:50 -03:00
0e037d0213 macOS Headless Fixes (#5167)
* Default hypervisor to disabled

* Include MVK on macOS

* Properly sign headless builds on macOS

* Force Vulkan on macOS

* Suggestions
2023-05-31 09:08:50 +02:00
0dca1fbe12 Add Context Menu Option to Run Application (#5154) 2023-05-30 19:51:03 +01:00
35d91a0e58 Linux: Automatically increase vm.max_map_count if it's too low (#4702)
* memory: Check results of pinvoke calls

* Increase vm.max_map_count when running Ryujinx

* Add SupportedOSPlatform attribute for WindowsApiException

* Revert increasing vm.max_map_count via script

* Add LinuxHelper to detect and increase vm.max_map_count

With GUI dialogs, this should be a bit more user-friendly.

* Supply arguments as a list to RunPkExec

* Add error logging in case RunPkExec() fails

* Prevent Gtk from crashing
2023-05-30 01:48:37 +02:00
a73a5d7e85 nuget: bump Microsoft.NET.Test.Sdk from 17.5.0 to 17.6.0 (#4986)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.5.0 to 17.6.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.5.0...v17.6.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 01:14:07 +02:00
832a5e8852 Make sure blend is disabled if render target has integer format (#5122)
* Make sure blend is disabled if render target has integer format

* Change approach to avoid permanently mutating state
2023-05-29 00:38:04 +02:00
96d1f0da2d Workaround for MoltenVK barrier issues (#5118) 2023-05-29 00:24:35 +02:00
597388ecda Fix incorrect vertex attribute format change (#5112)
* Fix incorrect vertex attribute format change

* Only change vertex format if the host supports the new format
2023-05-29 00:17:07 +02:00
1cf6d7b7bb Fix #5108: Allow surround sound for SDL2 in more scenarios (#5131) 2023-05-29 00:07:27 +02:00
7bc9d0cdad Linux: Use gamemode if it is available when using Ryujinx.sh. (#4938)
* Linux: Detect if gamemode is installed and start it when launching Ryujinx.

When using the Ryujinx.sh script to start the emulator check if gamemoderun exists and use it if it does.

Gamemode mode on Linux changes some system settings to make performance during gaming more consistent mainly by changing the CPU governor to performance.

https://github.com/FeralInteractive/gamemode

* Removed if statement.

* Fix due to wrong assumption about the output of which.

Checks if the which output contains a no match response, otherwise use gamemoderun.

Using a case statement because it makes substring matching possible in sh and also it turns out that adding an empty string after env throws an error because env attempts to parse it as a paramater.

* Missed a couple semicolons.

* Different approach for checking if gamemode is available.

Should hopefully work across all implementations of which.

* Remove unneeded which command.

* Change code to keep launch command to a single line.
2023-05-28 23:54:22 +02:00
dc0dbc50ab Add support for VK_EXT_depth_clip_control. (#5027)
* Add support for VK_EXT_depth_clip_control.

* Code review feedback

Minor formatting

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

* Check .DepthClipControl to make sure the host actually supports the feature.

* Review feedback: remove Vulkan platform switch, relying on QueryHostSupportsDepthClipControl to drive the behaviour - OpenGL returns true, and any future platforms that don't support the [-1, 1] depth mode can return false for the transformation.

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2023-05-28 23:31:56 +02:00
994f4dc77d chore: Update Avalonia to 0.10.21 (#5124) 2023-05-28 23:25:55 +02:00
c9e297b74c About window: Add changelog link under ver. number (#5095) 2023-05-28 23:13:40 +02:00
dd514a115c Update LastPlayed date on emulation end. (#5056) 2023-05-28 23:03:27 +02:00
7e0b4bd538 Improve macOS updater (#5064)
* Fix macOS Updater (once again)

* Also fix my brain's issues

* Move set -e that lsof doesn't trigger exit 1

* Resolve yesterdays brain malfunction 2

* Revert "Move set -e that lsof doesn't trigger exit 1"

This reverts commit 589a630610.

* Also check if PID exists

* Remove lsof and instead check for running processes

* Remove empty lines

* Increase max iterations

* Address feedback

* Remove obsolete check for child processes

* Update comments

* Update comments

* I swear this is the last commit

* lsof + ps check
2023-05-28 22:54:30 +02:00
378080eb87 Added Custom Path case when saving screenshots (#5086) 2023-05-28 22:44:46 +02:00
e8f5e97fa4 actions: revert timeout-minutes changes for PR workflow
Varibales aren't exposed to PRs...
2023-05-28 11:34:57 +02:00
f3873620a3 actions: Workaround YAML limitation for timeout-minutes
Because Github Actions wants an int, we use fromJSON to hack around
this.
2023-05-28 08:10:43 +02:00
986ac9ff83 Use variables to configure job timeouts (#5123) 2023-05-28 08:02:30 +02:00
42b9c1e8fe Ryujinx.Ava: fixes for random hangs on exit (#4827)
* Attempt at fixing hang on exit by ending the WindowNotificationManager notification loop, so that the Thread running it can exit.

* explicitly apply the NotificationManager template to allow the notification loop to begin

* NotificationHelper - remove explicity call to ApplyTemplate(). Change to ManualResetEventSlim so we can cancel the Wait on it.

* add a timeout to AudioRenderSystem.Stop()'s waiting for the termination signal, log a warning if this timeout occurs, and continue execution

* NotifiationHelper - cancel first, the CompleteAdding()

* Remove AudioRenderSystem._terminationEvent, redundant

* NotificationHelper - use host.Closing event to trigger cancellation instead of _notifationManager.DetachedFromLogicalTree

* Change NotificationHelper to use an explicit Thread for background work.  Wait on the cancellationToken's WaitHandle so the Thread doesn't have to deal with async. Wrap foreach in try/catch (OperationCanceledException) to swallow the escaping exception from the GetConsumingEnumerable().

* adjust formatting of AsyncWorkQueue constructor to use object initializers consistently

* use AsyncWorkQueue to do everything I added in SetNotificationManager()

* Revert "use AsyncWorkQueue to do everything I added in SetNotificationManager()"

This reverts commit f0e78366b8776ec8e2fef8ab023c0db1833155d3.

* use AsyncWorkQueue to handle the Thread-related changes previously made to NotificationHelper.SetNotificationHelper(). Wrap it in Lazy<T> and force instantiation in the TemplateApplied event handler to accomodate for the fact that AsyncWorkQueue starts immediately, and the notification dispatch loop was being delayed by _templateAppliedEvent.

* impl changes suggested by AcK77

* impl changes suggested by AcK77 (more)
2023-05-26 23:57:43 +02:00
3b375525fb Force reciprocal operation with value biased by constant to be precise on macOS (#5110)
* Force operations to be precise in some cases on SPIR-V

* Make it a bit more strict, add comments

* Shader cache version bump
2023-05-26 15:19:37 -03:00
e6658c133c Fix resolution scaling of image operation coordinates (#5102)
* Fix resolution scaling of image operation coordinates

* Shader cache version bump
2023-05-25 23:42:49 -03:00
5b42a4d2c4 Fix mod names (#5088) 2023-05-25 23:41:03 +02:00
8f0c89ffd6 Generate scaling helper functions on IR (#4714)
* Generate scaling helper functions on IR

* Delete unused code

* Split RewriteTextureSample and move gather bias add to an earlier pass

* Remove using

* Shader cache version bump
2023-05-25 17:46:58 -03:00
2c9715acf6 Truncate vertex attribute format if it exceeds stride on MoltenVK (#5094)
* Truncate vertex attribute format if it exceeds stride on MoltenVK

* Fix BGR format

* Move vertex attribute check to pipeline creation to avoid costs

* No need for this to be public
2023-05-25 17:03:51 -03:00
274af65f69 Update release.yml (#5058) 2023-05-25 16:17:37 +02:00
4ca78eded5 Vulkan: Do not set storage flag for multisample textures if not supported (#5060) 2023-05-23 10:41:37 +02:00
6cb6b15612 Implement p2rc, p2ri, p2rr and r2p.cc shaders (#5031)
* implement P2rC, P2rI, P2rR shaders

* implement R2p.CC shader

* bump CodeGenVersion

* address feedback
2023-05-22 17:32:15 -03:00
2725e40838 Revert "Bump MVK Version (#5057)" (#5061)
This reverts commit c2e4c8f98e.
2023-05-22 17:12:11 -03:00
c2e4c8f98e Bump MVK Version (#5057) 2023-05-22 14:41:08 -03:00
b53e7ffd46 Ava UI: Input Menu Redesign (#4990)
* Cleanup

* Remove redundant locales

* Start SVG Fixes…

Better +/- buttons

Fix the grips

Bumpers

Better directional pad

More SVG stuff

Grip adjustments

Final stuff

* Make image bigger

* Border radius

* More cleanup

* Restructure

* Restructure Rumble View

* Use compiled bindings where possible

* Round those pesky corners

* Ack Suggestions

* More suggestions

* Update src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs

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

---------

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-05-22 01:16:20 +02:00
ac66643346 Fix crash in SettingsViewModel when Vulkan isn't available (#4985)
* fix crash when Vulkan isn't available

* add VulkanRenderer.GetPhysicalDevices() overload that provides its own Vk API object and logs on failure

* adjustments per AcK77
2023-05-21 21:39:06 +02:00
21e88f17f6 ServerBase thread safety (#4577)
* Add guard against ServerBase.Dispose() being called multiple times. Add reset event to avoid Dispose() being called while the ServerLoop is still running.

* remove unused usings

* rework ServerBase to use one collection each for sessions and ports, and make all accesses thread-safe.

* fix Logger call

* use GetSessionObj(int) instead of using _sessions directly

* move _threadStopped check inside "dispose once" test

* - Replace _threadStopped event with attempt to Join() the ending thread (if that isn't the current thread) instead.

- Use the instance-local _selfProcess and (new) _selfThread variables to avoid suggesting that the current KProcess and KThread could change. Per gdkchan, they can't currently, and this old IPC system will be removed before that changes.

- Re-order Dispose() so that the Interlocked _isDisposed check is the last check before disposing, to increase the likelihood that multiple callers will result in one of them succeeding.

* code style suggestions per AcK77

* add infinite wait for thread termination
2023-05-21 21:28:51 +02:00
107 changed files with 2577 additions and 1293 deletions

View File

@ -27,7 +27,7 @@ jobs:
build:
name: ${{ matrix.OS_NAME }} (${{ matrix.configuration }})
runs-on: ${{ matrix.os }}
timeout-minutes: 35
timeout-minutes: 45
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
@ -110,7 +110,7 @@ jobs:
build_macos:
name: macOS Universal (${{ matrix.configuration }})
runs-on: ubuntu-latest
timeout-minutes: 35
timeout-minutes: 45
strategy:
matrix:
configuration: [ Debug, Release ]

View File

@ -12,7 +12,7 @@ concurrency: flatpak-release
jobs:
release:
timeout-minutes: 35
timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }}
runs-on: ubuntu-latest
env:

View File

@ -7,7 +7,7 @@ jobs:
pr_comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 35
timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }}
steps:
- uses: actions/github-script@v6
with:

View File

@ -33,7 +33,7 @@ jobs:
shell: bash
- name: Create tag
uses: actions/github-script@v5
uses: actions/github-script@v6
with:
script: |
github.rest.git.createRef({
@ -46,7 +46,7 @@ jobs:
release:
name: Release ${{ matrix.OS_NAME }}
runs-on: ${{ matrix.os }}
timeout-minutes: 35
timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }}
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
@ -144,7 +144,7 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-latest
timeout-minutes: 35
timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT) }}
steps:
- uses: actions/checkout@v3

View File

@ -3,11 +3,11 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="0.10.19" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="0.10.19" />
<PackageVersion Include="Avalonia.Desktop" Version="0.10.19" />
<PackageVersion Include="Avalonia.Diagnostics" Version="0.10.19" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="0.10.19" />
<PackageVersion Include="Avalonia" Version="0.10.21" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="0.10.21" />
<PackageVersion Include="Avalonia.Desktop" Version="0.10.21" />
<PackageVersion Include="Avalonia.Diagnostics" Version="0.10.21" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="0.10.21" />
<PackageVersion Include="Avalonia.Svg" Version="0.10.18" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="0.10.18" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
@ -21,7 +21,7 @@
<PackageVersion Include="LibHac" Version="0.18.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
<PackageVersion Include="NUnit" Version="3.13.3" />

View File

@ -11,4 +11,10 @@ if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL2" ]; then
RYUJINX_BIN="Ryujinx.Headless.SDL2"
fi
env DOTNET_EnableAlternateStackCheck=1 "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
COMMAND="env DOTNET_EnableAlternateStackCheck=1"
if command -v gamemoderun > /dev/null 2>&1; then
COMMAND="$COMMAND gamemoderun"
fi
$COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"

View File

@ -25,14 +25,27 @@ error_handler() {
exit 1
}
# Wait for Ryujinx to exit
# NOTE: in case no fds are open, lsof could be returning with a process still living.
# We wait 1s and assume the process stopped after that
lsof -p $APP_PID +r 1 &>/dev/null
sleep 1
trap 'error_handler ${LINENO}' ERR
# Wait for Ryujinx to exit.
# If the main process is still acitve, we wait for 1 second and check it again.
# After the fifth time checking, this script exits with status 1.
attempt=0
while true; do
if lsof -p $APP_PID +r 1 &>/dev/null || ps -p "$APP_PID" &>/dev/null; then
if [ "$attempt" -eq 4 ]; then
exit 1
fi
sleep 1
else
break
fi
(( attempt++ ))
done
sleep 1
# Now replace and reopen.
rm -rf "$INSTALL_DIRECTORY"
mv "$NEW_APP_DIRECTORY" "$INSTALL_DIRECTORY"

View File

@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Backends.SDL2
}
else
{
_supportSurroundConfiguration = spec.channels == 6;
_supportSurroundConfiguration = spec.channels >= 6;
}
}

View File

@ -31,7 +31,6 @@ namespace Ryujinx.Audio.Renderer.Server
private AudioRendererRenderingDevice _renderingDevice;
private AudioRendererExecutionMode _executionMode;
private IWritableEvent _systemEvent;
private ManualResetEvent _terminationEvent;
private MemoryPoolState _dspMemoryPoolState;
private VoiceContext _voiceContext;
private MixContext _mixContext;
@ -83,7 +82,6 @@ namespace Ryujinx.Audio.Renderer.Server
public AudioRenderSystem(AudioRendererManager manager, IWritableEvent systemEvent)
{
_manager = manager;
_terminationEvent = new ManualResetEvent(false);
_dspMemoryPoolState = MemoryPoolState.Create(MemoryPoolState.LocationType.Dsp);
_voiceContext = new VoiceContext();
_mixContext = new MixContext();
@ -387,11 +385,6 @@ namespace Ryujinx.Audio.Renderer.Server
_isActive = false;
}
if (_executionMode == AudioRendererExecutionMode.Auto)
{
_terminationEvent.WaitOne();
}
Logger.Info?.Print(LogClass.AudioRenderer, $"Stopped renderer id {_sessionId}");
}
@ -668,8 +661,6 @@ namespace Ryujinx.Audio.Renderer.Server
{
if (_isActive)
{
_terminationEvent.Reset();
if (!_manager.Processor.HasRemainingCommands(_sessionId))
{
GenerateCommandList(out CommandList commands);
@ -686,10 +677,6 @@ namespace Ryujinx.Audio.Renderer.Server
_isDspRunningBehind = true;
}
}
else
{
_terminationEvent.Set();
}
}
}
@ -857,7 +844,6 @@ namespace Ryujinx.Audio.Renderer.Server
}
_manager.Unregister(this);
_terminationEvent.Dispose();
_workBufferMemoryPin.Dispose();
if (MemoryManager is IRefCounted rc)

View File

@ -92,6 +92,8 @@ namespace Ryujinx.Ava
private bool _isActive;
private bool _renderingStarted;
private ManualResetEvent _gpuDoneEvent;
private IRenderer _renderer;
private readonly Thread _renderingThread;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
@ -183,6 +185,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
_gpuCancellationTokenSource = new CancellationTokenSource();
_gpuDoneEvent = new ManualResetEvent(false);
}
private void TopLevel_PointerEnterOrMoved(object sender, PointerEventArgs e)
@ -270,7 +273,7 @@ namespace Ryujinx.Ava
string directory = AppDataManager.Mode switch
{
AppDataManager.LaunchMode.Portable => Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
_ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx")
};
@ -423,10 +426,10 @@ namespace Ryujinx.Ava
_isActive = false;
if (_renderingThread.IsAlive)
{
_renderingThread.Join();
}
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
DisplaySleep.Restore();
@ -917,6 +920,14 @@ namespace Ryujinx.Ava
UpdateStatus();
}
}
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
});
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);

View File

@ -74,6 +74,13 @@
"GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)",
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
"StatusBarSystemVersion": "System Version: {0}",
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
"LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}",
"LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.",
"LinuxVmMaxMapCountDialogButtonUntilRestart": "Yes, until the next restart",
"LinuxVmMaxMapCountDialogButtonPersistent": "Yes, permanently",
"LinuxVmMaxMapCountWarningTextPrimary": "Max amount of memory mappings is lower than recommended.",
"LinuxVmMaxMapCountWarningTextSecondary": "The current value of vm.max_map_count ({0}) is lower than {1}. Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.\n\nYou might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.",
"Settings": "Settings",
"SettingsTabGeneral": "User Interface",
"SettingsTabGeneralGeneral": "General",
@ -216,26 +223,17 @@
"ControllerSettingsDPadDown": "Down",
"ControllerSettingsDPadLeft": "Left",
"ControllerSettingsDPadRight": "Right",
"ControllerSettingsStickButton": "Button",
"ControllerSettingsStickUp": "Up",
"ControllerSettingsStickDown": "Down",
"ControllerSettingsStickLeft": "Left",
"ControllerSettingsStickRight": "Right",
"ControllerSettingsStickStick": "Stick",
"ControllerSettingsStickInvertXAxis": "Invert Stick X",
"ControllerSettingsStickInvertYAxis": "Invert Stick Y",
"ControllerSettingsStickDeadzone": "Deadzone:",
"ControllerSettingsLStick": "Left Stick",
"ControllerSettingsLStickButton": "Button",
"ControllerSettingsLStickUp": "Up",
"ControllerSettingsLStickDown": "Down",
"ControllerSettingsLStickLeft": "Left",
"ControllerSettingsLStickRight": "Right",
"ControllerSettingsLStickStick": "Stick",
"ControllerSettingsLStickInvertXAxis": "Invert Stick X",
"ControllerSettingsLStickInvertYAxis": "Invert Stick Y",
"ControllerSettingsLStickDeadzone": "Deadzone:",
"ControllerSettingsRStick": "Right Stick",
"ControllerSettingsRStickButton": "Button",
"ControllerSettingsRStickUp": "Up",
"ControllerSettingsRStickDown": "Down",
"ControllerSettingsRStickLeft": "Left",
"ControllerSettingsRStickRight": "Right",
"ControllerSettingsRStickStick": "Stick",
"ControllerSettingsRStickInvertXAxis": "Invert Stick X",
"ControllerSettingsRStickInvertYAxis": "Invert Stick Y",
"ControllerSettingsRStickDeadzone": "Deadzone:",
"ControllerSettingsTriggersLeft": "Triggers Left",
"ControllerSettingsTriggersRight": "Triggers Right",
"ControllerSettingsTriggersButtonsLeft": "Trigger Buttons Left",
@ -291,6 +289,7 @@
"ControllerSettingsSaveProfileToolTip": "Save Profile",
"MenuBarFileToolsTakeScreenshot": "Take Screenshot",
"MenuBarFileToolsHideUi": "Hide UI",
"GameListContextMenuRunApplication": "Run Application",
"GameListContextMenuToggleFavorite": "Toggle Favorite",
"GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game",
"SettingsTabGeneralTheme": "Theme",
@ -629,7 +628,7 @@
"Search": "Search",
"UserProfilesRecoverLostAccounts": "Recover Lost Accounts",
"Recover": "Recover",
"UserProfilesRecoverHeading" : "Saves were found for the following accounts",
"UserProfilesRecoverHeading": "Saves were found for the following accounts",
"UserProfilesRecoverEmptyList": "No profiles to recover",
"GraphicsAATooltip": "Applies anti-aliasing to the game render",
"GraphicsAALabel": "Anti-Aliasing:",
@ -641,10 +640,12 @@
"SmaaMedium": "SMAA Medium",
"SmaaHigh": "SMAA High",
"SmaaUltra": "SMAA Ultra",
"UserEditorTitle" : "Edit User",
"UserEditorTitleCreate" : "Create User",
"UserEditorTitle": "Edit User",
"UserEditorTitleCreate": "Create User",
"SettingsTabNetworkInterface": "Network Interface:",
"NetworkInterfaceTooltip": "The network interface used for LAN features",
"NetworkInterfaceDefault": "Default",
"PackagingShaders": "Packaging Shaders"
}
"PackagingShaders": "Packaging Shaders",
"AboutChangelogButton": "View Changelog on GitHub",
"AboutChangelogButtonTooltipMessage": "Click to open the changelog for this version in your default browser."
}

View File

@ -18,6 +18,7 @@
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
<PublishSingleFile>true</PublishSingleFile>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>
@ -147,4 +148,4 @@
<ItemGroup>
<AdditionalFiles Include="Assets\Locales\en_US.json" />
</ItemGroup>
</Project>
</Project>

View File

@ -3,6 +3,9 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale">
<MenuItem
Click="RunApplication_Click"
Header="{locale:Locale GameListContextMenuRunApplication}" />
<MenuItem
Click="ToggleFavorite_Click"
Header="{locale:Locale GameListContextMenuToggleFavorite}"

View File

@ -323,5 +323,15 @@ namespace Ryujinx.Ava.UI.Controls
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, viewModel.SelectedApplication.Path, viewModel.SelectedApplication.TitleName);
}
}
public void RunApplication_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
viewModel.LoadApplication(viewModel.SelectedApplication.Path);
}
}
}
}

View File

@ -6,7 +6,6 @@ using Avalonia.Threading;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Logging;
using System;
@ -19,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
{
private static bool _isChoiceDialogOpen;
public async static Task<UserResult> ShowContentDialog(
private async static Task<UserResult> ShowContentDialog(
string title,
object content,
string primaryButton,
@ -67,7 +66,7 @@ namespace Ryujinx.Ava.UI.Helpers
return result;
}
private async static Task<UserResult> ShowTextDialog(
public async static Task<UserResult> ShowTextDialog(
string title,
string primaryText,
string secondaryText,
@ -319,7 +318,7 @@ namespace Ryujinx.Ava.UI.Helpers
Window parent = GetMainWindow();
if (parent != null && parent.IsActive && parent is MainWindow window && window.ViewModel.IsGameRunning)
if (parent is { IsActive: true } and MainWindow window && window.ViewModel.IsGameRunning)
{
contentDialogOverlayWindow = new()
{

View File

@ -3,21 +3,20 @@ using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Helpers
{
public static class NotificationHelper
{
private const int MaxNotifications = 4;
private const int MaxNotifications = 4;
private const int NotificationDelayInMs = 5000;
private static WindowNotificationManager _notificationManager;
private static readonly ManualResetEvent _templateAppliedEvent = new(false);
private static readonly BlockingCollection<Notification> _notifications = new();
public static void SetNotificationManager(Window host)
@ -29,25 +28,31 @@ namespace Ryujinx.Ava.UI.Helpers
Margin = new Thickness(0, 0, 15, 40)
};
var maybeAsyncWorkQueue = new Lazy<AsyncWorkQueue<Notification>>(
() => new AsyncWorkQueue<Notification>(notification =>
{
Dispatcher.UIThread.Post(() =>
{
_notificationManager.Show(notification);
});
},
"UI.NotificationThread",
_notifications),
LazyThreadSafetyMode.ExecutionAndPublication);
_notificationManager.TemplateApplied += (sender, args) =>
{
_templateAppliedEvent.Set();
// NOTE: Force creation of the AsyncWorkQueue.
_ = maybeAsyncWorkQueue.Value;
};
Task.Run(async () =>
host.Closing += (sender, args) =>
{
_templateAppliedEvent.WaitOne();
foreach (var notification in _notifications.GetConsumingEnumerable())
if (maybeAsyncWorkQueue.IsValueCreated)
{
Dispatcher.UIThread.Post(() =>
{
_notificationManager.Show(notification);
});
await Task.Delay(NotificationDelayInMs / MaxNotifications);
maybeAsyncWorkQueue.Value.Dispose();
}
});
};
}
public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null)

View File

@ -7,6 +7,7 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Views.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@ -30,7 +31,7 @@ using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.ViewModels
{
public class ControllerSettingsViewModel : BaseModel, IDisposable
public class ControllerInputViewModel : BaseModel, IDisposable
{
private const string Disabled = "disabled";
private const string ProControllerResource = "Ryujinx.Ui.Common/Resources/Controller_ProCon.svg";
@ -231,7 +232,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public InputConfig Config { get; set; }
public ControllerSettingsViewModel(UserControl owner) : this()
public ControllerInputViewModel(UserControl owner) : this()
{
_owner = owner;
@ -258,7 +259,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public ControllerSettingsViewModel()
public ControllerInputViewModel()
{
PlayerIndexes = new ObservableCollection<PlayerModel>();
Controllers = new ObservableCollection<ControllerModel>();
@ -328,12 +329,12 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void ShowMotionConfig()
{
await MotionSettingsWindow.Show(this);
await MotionInputView.Show(this);
}
public async void ShowRumbleConfig()
{
await RumbleSettingsWindow.Show(this);
await RumbleInputView.Show(this);
}
private void LoadInputDriver()

View File

@ -1529,6 +1529,8 @@ namespace Ryujinx.Ava.UI.ViewModels
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
}
appMetadata.LastPlayed = DateTime.UtcNow;
});
}

View File

@ -0,0 +1,93 @@
namespace Ryujinx.Ava.UI.ViewModels
{
public class MotionInputViewModel : BaseModel
{
private int _slot;
public int Slot
{
get => _slot;
set
{
_slot = value;
OnPropertyChanged();
}
}
private int _altSlot;
public int AltSlot
{
get => _altSlot;
set
{
_altSlot = value;
OnPropertyChanged();
}
}
private string _dsuServerHost;
public string DsuServerHost
{
get => _dsuServerHost;
set
{
_dsuServerHost = value;
OnPropertyChanged();
}
}
private int _dsuServerPort;
public int DsuServerPort
{
get => _dsuServerPort;
set
{
_dsuServerPort = value;
OnPropertyChanged();
}
}
private bool _mirrorInput;
public bool MirrorInput
{
get => _mirrorInput;
set
{
_mirrorInput = value;
OnPropertyChanged();
}
}
private int _sensitivity;
public int Sensitivity
{
get => _sensitivity;
set
{
_sensitivity = value;
OnPropertyChanged();
}
}
private double _gryoDeadzone;
public double GyroDeadzone
{
get => _gryoDeadzone;
set
{
_gryoDeadzone = value;
OnPropertyChanged();
}
}
private bool _enableCemuHookMotion;
public bool EnableCemuHookMotion
{
get => _enableCemuHookMotion;
set
{
_enableCemuHookMotion = value;
OnPropertyChanged();
}
}
}
}

View File

@ -0,0 +1,27 @@
namespace Ryujinx.Ava.UI.ViewModels
{
public class RumbleInputViewModel : BaseModel
{
private float _strongRumble;
public float StrongRumble
{
get => _strongRumble;
set
{
_strongRumble = value;
OnPropertyChanged();
}
}
private float _weakRumble;
public float WeakRumble
{
get => _weakRumble;
set
{
_weakRumble = value;
OnPropertyChanged();
}
}
}
}

View File

@ -311,7 +311,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
_gpuIds = new List<string>();
List<string> names = new();
var devices = VulkanRenderer.GetPhysicalDevices(Vk.GetApi());
var devices = VulkanRenderer.GetPhysicalDevices();
if (devices.Length == 0)
{

View File

@ -4,7 +4,6 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
@ -13,18 +12,18 @@ using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
namespace Ryujinx.Ava.UI.Windows
namespace Ryujinx.Ava.UI.Views.Input
{
public partial class ControllerSettingsWindow : UserControl
public partial class ControllerInputView : UserControl
{
private bool _dialogOpen;
private ButtonKeyAssigner _currentAssigner;
internal ControllerSettingsViewModel ViewModel { get; set; }
internal ControllerInputViewModel ViewModel { get; set; }
public ControllerSettingsWindow()
public ControllerInputView()
{
DataContext = ViewModel = new ControllerSettingsViewModel(this);
DataContext = ViewModel = new ControllerInputViewModel(this);
InitializeComponent();

View File

@ -1,12 +1,15 @@
<UserControl
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.UI.Windows.MotionSettingsWindow"
x:Class="Ryujinx.Ava.UI.Views.Input.MotionInputView"
x:CompileBindings="True"
x:DataType="viewModels:MotionInputViewModel"
Focusable="True">
<Grid Margin="10">
<Grid.RowDefinitions>
@ -14,7 +17,9 @@
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel
Orientation="Horizontal"
HorizontalAlignment="Center">
<TextBlock
Margin="0"
HorizontalAlignment="Center"
@ -28,11 +33,14 @@
Maximum="100"
Minimum="0"
Value="{Binding Sensitivity, Mode=TwoWay}" />
<TextBlock HorizontalAlignment="Center"
Margin="5, 0"
Text="{Binding Sensitivity, StringFormat=\{0:0\}%}" />
<TextBlock
HorizontalAlignment="Center"
Margin="5, 0"
Text="{Binding Sensitivity, StringFormat=\{0:0\}%}" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel
Orientation="Horizontal"
HorizontalAlignment="Center">
<TextBlock
Margin="0"
HorizontalAlignment="Center"
@ -51,17 +59,25 @@
Margin="5, 0"
Text="{Binding GyroDeadzone, StringFormat=\{0:0.00\}}" />
</StackPanel>
<Separator Height="1" Margin="0,5" />
<CheckBox Margin="5" IsChecked="{Binding EnableCemuHookMotion}">
<TextBlock Margin="0,3,0,0" VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionUseCemuhookCompatibleMotion}" />
<Separator
Height="1"
Margin="0,5" />
<CheckBox
Margin="5"
IsChecked="{Binding EnableCemuHookMotion}">
<TextBlock
Margin="0,3,0,0"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionUseCemuhookCompatibleMotion}" />
</CheckBox>
</StackPanel>
<Border Grid.Row="1"
Padding="20,5"
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
HorizontalAlignment="Stretch">
<Border
Grid.Row="1"
Padding="20,5"
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5"
HorizontalAlignment="Stretch">
<Grid VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -109,30 +125,42 @@
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Margin="0,10,0,0" VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionControllerSlot}" />
<ui:NumberBox Grid.Row="0" Grid.Column="1"
Name="CemuHookSlotUpDown"
SmallChange="1"
LargeChange="1"
Maximum="4"
Minimum="0"
Value="{Binding Slot}" />
<TextBlock Margin="0,10,0,0" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionRightJoyConSlot}" />
<ui:NumberBox Grid.Row="1" Grid.Column="1"
Name="CemuHookRightJoyConSlotUpDown"
SmallChange="1"
LargeChange="1"
Maximum="4"
Minimum="0"
Value="{Binding AltSlot}" />
<TextBlock
Margin="0,10,0,0"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionControllerSlot}" />
<ui:NumberBox
Grid.Row="0"
Grid.Column="1"
Name="CemuHookSlotUpDown"
SmallChange="1"
LargeChange="1"
Maximum="4"
Minimum="0"
Value="{Binding Slot}" />
<TextBlock
Margin="0,10,0,0"
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionRightJoyConSlot}" />
<ui:NumberBox
Grid.Row="1"
Grid.Column="1"
Name="CemuHookRightJoyConSlotUpDown"
SmallChange="1"
LargeChange="1"
Maximum="4"
Minimum="0"
Value="{Binding AltSlot}" />
</Grid>
</StackPanel>
<CheckBox HorizontalAlignment="Center"
IsChecked="{Binding MirrorInput, Mode=TwoWay}">
<TextBlock HorizontalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionMirrorInput}" />
<CheckBox
HorizontalAlignment="Center"
IsChecked="{Binding MirrorInput, Mode=TwoWay}">
<TextBlock
HorizontalAlignment="Center"
Text="{locale:Locale ControllerSettingsMotionMirrorInput}" />
</CheckBox>
</StackPanel>
</Grid>

View File

@ -6,44 +6,42 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid.Controller;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows
namespace Ryujinx.Ava.UI.Views.Input
{
public partial class MotionSettingsWindow : UserControl
public partial class MotionInputView : UserControl
{
private readonly InputConfiguration<GamepadInputId, StickInputId> _viewmodel;
private MotionInputViewModel _viewModel;
public MotionSettingsWindow()
public MotionInputView()
{
InitializeComponent();
DataContext = _viewmodel;
}
public MotionSettingsWindow(ControllerSettingsViewModel viewmodel)
public MotionInputView(ControllerInputViewModel viewModel)
{
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
var config = viewModel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
_viewmodel = new InputConfiguration<GamepadInputId, StickInputId>()
_viewModel = new MotionInputViewModel
{
Slot = config.Slot,
AltSlot = config.AltSlot,
DsuServerHost = config.DsuServerHost,
DsuServerPort = config.DsuServerPort,
MirrorInput = config.MirrorInput,
EnableMotion = config.EnableMotion,
Sensitivity = config.Sensitivity,
GyroDeadzone = config.GyroDeadzone,
EnableCemuHookMotion = config.EnableCemuHookMotion
};
InitializeComponent();
DataContext = _viewmodel;
DataContext = _viewModel;
}
public static async Task Show(ControllerSettingsViewModel viewmodel)
public static async Task Show(ControllerInputViewModel viewModel)
{
MotionSettingsWindow content = new MotionSettingsWindow(viewmodel);
MotionInputView content = new(viewModel);
ContentDialog contentDialog = new ContentDialog
ContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.ControllerMotionTitle],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsSave],
@ -53,16 +51,15 @@ namespace Ryujinx.Ava.UI.Windows
};
contentDialog.PrimaryButtonClick += (sender, args) =>
{
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
config.Slot = content._viewmodel.Slot;
config.EnableMotion = content._viewmodel.EnableMotion;
config.Sensitivity = content._viewmodel.Sensitivity;
config.GyroDeadzone = content._viewmodel.GyroDeadzone;
config.AltSlot = content._viewmodel.AltSlot;
config.DsuServerHost = content._viewmodel.DsuServerHost;
config.DsuServerPort = content._viewmodel.DsuServerPort;
config.EnableCemuHookMotion = content._viewmodel.EnableCemuHookMotion;
config.MirrorInput = content._viewmodel.MirrorInput;
var config = viewModel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
config.Slot = content._viewModel.Slot;
config.Sensitivity = content._viewModel.Sensitivity;
config.GyroDeadzone = content._viewModel.GyroDeadzone;
config.AltSlot = content._viewModel.AltSlot;
config.DsuServerHost = content._viewModel.DsuServerHost;
config.DsuServerPort = content._viewModel.DsuServerPort;
config.EnableCemuHookMotion = content._viewModel.EnableCemuHookMotion;
config.MirrorInput = content._viewModel.MirrorInput;
};
await contentDialog.ShowAsync();

View File

@ -1,11 +1,14 @@
<UserControl
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.UI.Windows.RumbleSettingsWindow"
x:Class="Ryujinx.Ava.UI.Views.Input.RumbleInputView"
x:DataType="viewModels:RumbleInputViewModel"
x:CompileBindings="True"
Focusable="True">
<Grid Margin="10">
<Grid.RowDefinitions>

View File

@ -6,36 +6,37 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid.Controller;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows
namespace Ryujinx.Ava.UI.Views.Input
{
public partial class RumbleSettingsWindow : UserControl
public partial class RumbleInputView : UserControl
{
private readonly InputConfiguration<GamepadInputId, StickInputId> _viewmodel;
private RumbleInputViewModel _viewModel;
public RumbleSettingsWindow()
public RumbleInputView()
{
InitializeComponent();
DataContext = _viewmodel;
}
public RumbleSettingsWindow(ControllerSettingsViewModel viewmodel)
public RumbleInputView(ControllerInputViewModel viewModel)
{
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
var config = viewModel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
_viewmodel = new InputConfiguration<GamepadInputId, StickInputId>()
_viewModel = new RumbleInputViewModel
{
StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble
StrongRumble = config.StrongRumble,
WeakRumble = config.WeakRumble
};
InitializeComponent();
DataContext = _viewmodel;
DataContext = _viewModel;
}
public static async Task Show(ControllerSettingsViewModel viewmodel)
public static async Task Show(ControllerInputViewModel viewModel)
{
RumbleSettingsWindow content = new RumbleSettingsWindow(viewmodel);
RumbleInputView content = new(viewModel);
ContentDialog contentDialog = new ContentDialog
ContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.ControllerRumbleTitle],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsSave],
@ -43,14 +44,14 @@ namespace Ryujinx.Ava.UI.Windows
CloseButtonText = LocaleManager.Instance[LocaleKeys.ControllerSettingsClose],
Content = content,
};
contentDialog.PrimaryButtonClick += (sender, args) =>
{
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
config.StrongRumble = content._viewmodel.StrongRumble;
config.WeakRumble = content._viewmodel.WeakRumble;
var config = viewModel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
config.StrongRumble = content._viewModel.StrongRumble;
config.WeakRumble = content._viewModel.WeakRumble;
};
await contentDialog.ShowAsync();
}
}

View File

@ -1,11 +1,11 @@
<UserControl
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsInputView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:views="clr-namespace:Ryujinx.Ava.UI.Views.Input"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:CompileBindings="True"
@ -13,34 +13,56 @@
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
<ScrollViewer
Name="InputPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel Margin="4" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
IsChecked="{Binding EnableDockedMode}">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
</CheckBox>
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
IsChecked="{Binding EnableKeyboard}">
<TextBlock Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
</CheckBox>
<CheckBox Margin="5,0"
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
IsChecked="{Binding EnableMouse}">
<TextBlock Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
</CheckBox>
</StackPanel>
<window:ControllerSettingsWindow Name="ControllerSettings" Margin="0" MinHeight="600" />
</StackPanel>
<Panel
Margin="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<views:ControllerInputView
Grid.Row="0"
Name="ControllerSettings" />
<StackPanel
Orientation="Vertical"
Grid.Row="2">
<Separator
Margin="0 10"
Height="1" />
<StackPanel
Orientation="Horizontal"
Spacing="10">
<CheckBox
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
MinWidth="0"
IsChecked="{Binding EnableDockedMode}">
<TextBlock
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
IsChecked="{Binding EnableKeyboard}">
<TextBlock
Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
IsChecked="{Binding EnableMouse}">
<TextBlock
Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
</CheckBox>
</StackPanel>
</StackPanel>
</Grid>
</Panel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -58,11 +58,20 @@
JustifyContent="SpaceAround"
RowSpacing="2">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="28"
FontWeight="Bold"
Text="Ryujinx"
TextAlignment="Left" />
<TextBlock Text="(REE-YOU-JINX)" TextAlignment="Left" />
TextAlignment="Center"
Width="100" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
Text="(REE-YOU-JINX)"
TextAlignment="Center"
Width="100" />
</flex:FlexPanel>
</Grid>
<TextBlock
@ -72,6 +81,18 @@
LineHeight="12"
Text="{Binding Version}"
TextAlignment="Center" />
<Button
Padding="5"
HorizontalAlignment="Center"
Background="Transparent"
Click="Button_OnClick"
Tag="https://github.com/Ryujinx/Ryujinx/wiki/Changelog#ryujinx-changelog">
<TextBlock
FontSize="10"
Text="{locale:Locale AboutChangelogButton}"
TextAlignment="Center"
ToolTip.Tip="{locale:Locale AboutChangelogButtonTooltipMessage}" />
</Button>
</StackPanel>
<StackPanel
Grid.Row="2"

View File

@ -23,6 +23,7 @@ using Ryujinx.Ui.Common.Helper;
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using InputManager = Ryujinx.Input.HLE.InputManager;
@ -258,7 +259,64 @@ namespace Ryujinx.Ava.UI.Windows
ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this);
}
protected void CheckLaunchState()
[SupportedOSPlatform("linux")]
private static async void ShowVmMaxMapCountWarning()
{
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary,
LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount);
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextPrimary],
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary]
);
}
[SupportedOSPlatform("linux")]
private static async void ShowVmMaxMapCountDialog()
{
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary,
LinuxHelper.RecommendedVmMaxMapCount);
UserResult response = await ContentDialogHelper.ShowTextDialog(
$"Ryujinx - {LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTitle]}",
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary],
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextSecondary],
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonUntilRestart],
LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonPersistent],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
(int)Symbol.Help
);
int rc;
switch (response)
{
case UserResult.Ok:
rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}");
if (rc == 0)
{
Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart.");
}
else
{
Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}");
}
break;
case UserResult.No:
rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}");
if (rc == 0)
{
Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}");
}
else
{
Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}");
}
break;
}
}
private void CheckLaunchState()
{
if (ShowKeyErrorOnLoad)
{
@ -268,6 +326,20 @@ namespace Ryujinx.Ava.UI.Windows
UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, this));
}
if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
{
Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({LinuxHelper.VmMaxMapCount})");
if (LinuxHelper.PkExecPath is not null)
{
Dispatcher.UIThread.Post(ShowVmMaxMapCountDialog);
}
else
{
Dispatcher.UIThread.Post(ShowVmMaxMapCountWarning);
}
}
if (_deferLoad)
{
_deferLoad = false;

View File

@ -22,9 +22,11 @@ namespace Ryujinx.Common
_cts = new CancellationTokenSource();
_queue = collection;
_workerAction = callback;
_workerThread = new Thread(DoWork) { Name = name };
_workerThread.IsBackground = true;
_workerThread = new Thread(DoWork)
{
Name = name,
IsBackground = true
};
_workerThread.Start();
}

View File

@ -39,6 +39,7 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsViewportMask;
public readonly bool SupportsViewportSwizzle;
public readonly bool SupportsIndirectParameters;
public readonly bool SupportsDepthClipControl;
public readonly uint MaximumUniformBuffersPerStage;
public readonly uint MaximumStorageBuffersPerStage;
@ -85,6 +86,7 @@ namespace Ryujinx.Graphics.GAL
bool supportsViewportMask,
bool supportsViewportSwizzle,
bool supportsIndirectParameters,
bool supportsDepthClipControl,
uint maximumUniformBuffersPerStage,
uint maximumStorageBuffersPerStage,
uint maximumTexturesPerStage,
@ -127,6 +129,7 @@ namespace Ryujinx.Graphics.GAL
SupportsViewportMask = supportsViewportMask;
SupportsViewportSwizzle = supportsViewportSwizzle;
SupportsIndirectParameters = supportsIndirectParameters;
SupportsDepthClipControl = supportsDepthClipControl;
MaximumUniformBuffersPerStage = maximumUniformBuffersPerStage;
MaximumStorageBuffersPerStage = maximumStorageBuffersPerStage;
MaximumTexturesPerStage = maximumTexturesPerStage;

View File

@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration;
using System;
using System.Threading;
namespace Ryujinx.Graphics.GAL
{
@ -52,7 +53,7 @@ namespace Ryujinx.Graphics.GAL
void ResetCounter(CounterType type);
void RunLoop(Action gpuLoop)
void RunLoop(ThreadStart gpuLoop)
{
gpuLoop();
}

View File

@ -30,7 +30,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
private IRenderer _baseRenderer;
private Thread _gpuThread;
private Thread _backendThread;
private bool _disposed;
private bool _running;
private AutoResetEvent _frameComplete = new AutoResetEvent(true);
@ -98,19 +97,17 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_refQueue = new object[MaxRefsPerCommand * QueueCount];
}
public void RunLoop(Action gpuLoop)
public void RunLoop(ThreadStart gpuLoop)
{
_running = true;
_backendThread = Thread.CurrentThread;
_gpuThread = new Thread(() => {
gpuLoop();
_running = false;
_galWorkAvailable.Set();
});
_gpuThread = new Thread(gpuLoop)
{
Name = "GPU.MainThread"
};
_gpuThread.Name = "GPU.MainThread";
_gpuThread.Start();
RenderLoop();
@ -120,7 +117,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
{
// Power through the render queue until the Gpu thread work is done.
while (_running && !_disposed)
while (_running)
{
_galWorkAvailable.Wait();
_galWorkAvailable.Reset();
@ -488,12 +485,23 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return _baseRenderer.PrepareHostMapping(address, size);
}
public void FlushThreadedCommands()
{
SpinWait wait = new();
while (Volatile.Read(ref _commandCount) > 0)
{
wait.SpinOnce();
}
}
public void Dispose()
{
// Dispose must happen from the render thread, after all commands have completed.
// Stop the GPU thread.
_disposed = true;
_running = false;
_galWorkAvailable.Set();
if (_gpuThread != null && _gpuThread.IsAlive)
{

View File

@ -63,6 +63,8 @@ namespace Ryujinx.Graphics.GAL
public bool PrimitiveRestartEnable;
public uint PatchControlPoints;
public DepthMode DepthMode;
public void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs)
{
VertexAttribCount = vertexAttribs.Length;

View File

@ -771,7 +771,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
private void UpdateDepthMode()
{
_context.Renderer.Pipeline.SetDepthMode(GetDepthMode());
DepthMode mode = GetDepthMode();
_pipeline.DepthMode = mode;
_context.Renderer.Pipeline.SetDepthMode(mode);
}
/// <summary>

View File

@ -390,7 +390,6 @@ namespace Ryujinx.Graphics.Gpu
/// </summary>
public void Dispose()
{
Renderer.Dispose();
GPFifo.Dispose();
HostInitalized.Dispose();
@ -403,6 +402,8 @@ namespace Ryujinx.Graphics.Gpu
PhysicalMemoryRegistry.Clear();
RunDeferredActions();
Renderer.Dispose();
}
}
}

View File

@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
private const uint CodeGenVersion = 4646;
private const uint CodeGenVersion = 5027;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";

View File

@ -17,8 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly ResourceCounts _resourceCounts;
private readonly int _stageIndex;
private readonly int[] _constantBufferBindings;
/// <summary>
/// Creates a new GPU accessor.
/// </summary>
@ -28,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
_context = context;
_resourceCounts = resourceCounts;
_stageIndex = stageIndex;
if (context.Capabilities.Api != TargetApi.Vulkan)
{
_constantBufferBindings = new int[Constants.TotalGpUniformBuffers];
_constantBufferBindings.AsSpan().Fill(-1);
}
}
public int QueryBindingConstantBuffer(int index)
@ -45,15 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
}
else
{
int binding = _constantBufferBindings[index];
if (binding < 0)
{
binding = _resourceCounts.UniformBuffersCount++;
_constantBufferBindings[index] = binding;
}
return binding;
return _resourceCounts.UniformBuffersCount++;
}
}
@ -165,6 +149,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
public bool QueryHostSupportsViewportMask() => _context.Capabilities.SupportsViewportMask;
public bool QueryHostSupportsDepthClipControl() => _context.Capabilities.SupportsDepthClipControl;
/// <summary>
/// Converts a packed Maxwell texture format to the shader translator texture format.
/// </summary>

View File

@ -736,6 +736,19 @@ namespace Ryujinx.Graphics.Gpu.Shader
return MatchesTexture(specializationState, descriptor);
}
/// <summary>
/// Populates pipeline state that doesn't exist in older caches with default values
/// based on specialization state.
/// </summary>
/// <param name="pipelineState">Pipeline state to prepare</param>
private void PreparePipelineState(ref ProgramPipelineState pipelineState)
{
if (!_compute)
{
pipelineState.DepthMode = GraphicsState.DepthMode ? DepthMode.MinusOneToOne : DepthMode.ZeroToOne;
}
}
/// <summary>
/// Reads shader specialization state that has been serialized.
/// </summary>
@ -776,6 +789,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
ProgramPipelineState pipelineState = default;
dataReader.ReadWithMagicAndSize(ref pipelineState, PgpsMagic);
specState.PreparePipelineState(ref pipelineState);
specState.PipelineState = pipelineState;
}

View File

@ -163,6 +163,7 @@ namespace Ryujinx.Graphics.OpenGL
supportsViewportMask: HwCapabilities.SupportsViewportArray2,
supportsViewportSwizzle: HwCapabilities.SupportsViewportSwizzle,
supportsIndirectParameters: HwCapabilities.SupportsIndirectParameters,
supportsDepthClipControl: true,
maximumUniformBuffersPerStage: 13, // TODO: Avoid hardcoding those limits here and get from driver?
maximumStorageBuffersPerStage: 16,
maximumTexturesPerStage: 32,

View File

@ -239,33 +239,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
context.AppendLine();
}
bool isFragment = context.Config.Stage == ShaderStage.Fragment;
if (isFragment || context.Config.Stage == ShaderStage.Compute || context.Config.Stage == ShaderStage.Vertex)
if (context.Config.Stage == ShaderStage.Fragment && context.Config.GpuAccessor.QueryEarlyZForce())
{
if (isFragment && context.Config.GpuAccessor.QueryEarlyZForce())
{
context.AppendLine("layout(early_fragment_tests) in;");
context.AppendLine();
}
if ((context.Config.UsedFeatures & (FeatureFlags.FragCoordXY | FeatureFlags.IntegerSampling)) != 0)
{
string stage = OperandManager.GetShaderStagePrefix(context.Config.Stage);
int scaleElements = context.Config.GetTextureDescriptors().Length + context.Config.GetImageDescriptors().Length;
if (isFragment)
{
scaleElements++; // Also includes render target scale, for gl_FragCoord.
}
if (context.Config.UsedFeatures.HasFlag(FeatureFlags.IntegerSampling) && scaleElements != 0)
{
AppendHelperFunction(context, $"Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_{stage}.glsl");
context.AppendLine();
}
}
context.AppendLine("layout(early_fragment_tests) in;");
context.AppendLine();
}
if ((info.HelperFunctionsMask & HelperFunctionsMask.AtomicMinMaxS32Shared) != 0)

View File

@ -1,19 +0,0 @@
ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex)
{
float scale = support_buffer.s_render_scale[1 + samplerIndex];
if (scale == 1.0)
{
return inputVec;
}
return ivec2(vec2(inputVec) * scale);
}
int Helper_TextureSizeUnscale(int size, int samplerIndex)
{
float scale = support_buffer.s_render_scale[1 + samplerIndex];
if (scale == 1.0)
{
return size;
}
return int(float(size) / scale);
}

View File

@ -1,26 +0,0 @@
ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex)
{
float scale = support_buffer.s_render_scale[1 + samplerIndex];
if (scale == 1.0)
{
return inputVec;
}
if (scale < 0.0) // If less than 0, try interpolate between texels by using the screen position.
{
return ivec2(vec2(inputVec) * (-scale) + mod(gl_FragCoord.xy, 0.0 - scale));
}
else
{
return ivec2(vec2(inputVec) * scale);
}
}
int Helper_TextureSizeUnscale(int size, int samplerIndex)
{
float scale = abs(support_buffer.s_render_scale[1 + samplerIndex]);
if (scale == 1.0)
{
return size;
}
return int(float(size) / scale);
}

View File

@ -1,20 +0,0 @@
ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex)
{
float scale = abs(support_buffer.s_render_scale[1 + samplerIndex + support_buffer.s_frag_scale_count]);
if (scale == 1.0)
{
return inputVec;
}
return ivec2(vec2(inputVec) * scale);
}
int Helper_TextureSizeUnscale(int size, int samplerIndex)
{
float scale = abs(support_buffer.s_render_scale[1 + samplerIndex + support_buffer.s_frag_scale_count]);
if (scale == 1.0)
{
return size;
}
return int(float(size) / scale);
}

View File

@ -101,6 +101,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
Add(Instruction.MemoryBarrier, InstType.CallNullary, "memoryBarrier");
Add(Instruction.Minimum, InstType.CallBinary, "min");
Add(Instruction.MinimumU32, InstType.CallBinary, "min");
Add(Instruction.Modulo, InstType.CallBinary, "mod");
Add(Instruction.Multiply, InstType.OpBinaryCom, "*", 1);
Add(Instruction.MultiplyHighS32, InstType.CallBinary, HelperFunctionNames.MultiplyHighS32);
Add(Instruction.MultiplyHighU32, InstType.CallBinary, HelperFunctionNames.MultiplyHighU32);

View File

@ -97,30 +97,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
texCallBuilder.Append(str);
}
string ApplyScaling(string vector)
{
if (context.Config.Stage.SupportsRenderScale() &&
texOp.Inst == Instruction.ImageLoad &&
!isBindless &&
!isIndexed)
{
// Image scales start after texture ones.
int scaleIndex = context.Config.GetTextureDescriptors().Length + context.Config.FindImageDescriptorIndex(texOp);
if (pCount == 3 && isArray)
{
// The array index is not scaled, just x and y.
vector = $"ivec3(Helper_TexelFetchScale(({vector}).xy, {scaleIndex}), ({vector}).z)";
}
else if (pCount == 2 && !isArray)
{
vector = $"Helper_TexelFetchScale({vector}, {scaleIndex})";
}
}
return vector;
}
if (pCount > 1)
{
string[] elems = new string[pCount];
@ -130,7 +106,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
elems[index] = Src(AggregateType.S32);
}
Append(ApplyScaling($"ivec{pCount}({string.Join(", ", elems)})"));
Append($"ivec{pCount}({string.Join(", ", elems)})");
}
else
{
@ -584,53 +560,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
}
}
string ApplyScaling(string vector)
{
if (intCoords)
{
if (context.Config.Stage.SupportsRenderScale() &&
!isBindless &&
!isIndexed)
{
int index = context.Config.FindTextureDescriptorIndex(texOp);
if (pCount == 3 && isArray)
{
// The array index is not scaled, just x and y.
vector = "ivec3(Helper_TexelFetchScale((" + vector + ").xy, " + index + "), (" + vector + ").z)";
}
else if (pCount == 2 && !isArray)
{
vector = "Helper_TexelFetchScale(" + vector + ", " + index + ")";
}
}
}
return vector;
}
string ApplyBias(string vector)
{
int gatherBiasPrecision = context.Config.GpuAccessor.QueryHostGatherBiasPrecision();
if (isGather && gatherBiasPrecision != 0)
{
// GPU requires texture gather to be slightly offset to match NVIDIA behaviour when point is exactly between two texels.
// Offset by the gather precision divided by 2 to correct for rounding.
if (pCount == 1)
{
vector = $"{vector} + (1.0 / (float(textureSize({samplerName}, 0)) * float({1 << (gatherBiasPrecision + 1)})))";
}
else
{
vector = $"{vector} + (1.0 / (vec{pCount}(textureSize({samplerName}, 0).{"xyz".Substring(0, pCount)}) * float({1 << (gatherBiasPrecision + 1)})))";
}
}
return vector;
}
Append(ApplyBias(ApplyScaling(AssemblePVector(pCount))));
Append(AssemblePVector(pCount));
string AssembleDerivativesVector(int count)
{
@ -750,7 +680,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
}
else
{
(TextureDescriptor descriptor, int descriptorIndex) = context.Config.FindTextureDescriptor(texOp);
TextureDescriptor descriptor = context.Config.FindTextureDescriptor(texOp);
bool hasLod = !descriptor.Type.HasFlag(SamplerType.Multisample) && descriptor.Type != SamplerType.TextureBuffer;
string texCall;
@ -767,14 +697,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
texCall = $"textureSize({samplerName}){GetMask(texOp.Index)}";
}
if (context.Config.Stage.SupportsRenderScale() &&
(texOp.Index < 2 || (texOp.Type & SamplerType.Mask) == SamplerType.Texture3D) &&
!isBindless &&
!isIndexed)
{
texCall = $"Helper_TextureSizeUnscale({texCall}, {descriptorIndex})";
}
return texCall;
}
}

View File

@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
public Dictionary<IoDefinition, Instruction> OutputsPerPatch { get; } = new Dictionary<IoDefinition, Instruction>();
public Instruction CoordTemp { get; set; }
public StructuredFunction CurrentFunction { get; set; }
private readonly Dictionary<AstOperand, Instruction> _locals = new Dictionary<AstOperand, Instruction>();
private readonly Dictionary<int, Instruction[]> _localForArgs = new Dictionary<int, Instruction[]>();
private readonly Dictionary<int, Instruction> _funcArgs = new Dictionary<int, Instruction>();

View File

@ -4,7 +4,6 @@ using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using static Spv.Specification;
@ -114,6 +113,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
Add(Instruction.MemoryBarrier, GenerateMemoryBarrier);
Add(Instruction.Minimum, GenerateMinimum);
Add(Instruction.MinimumU32, GenerateMinimumU32);
Add(Instruction.Modulo, GenerateModulo);
Add(Instruction.Multiply, GenerateMultiply);
Add(Instruction.MultiplyHighS32, GenerateMultiplyHighS32);
Add(Instruction.MultiplyHighU32, GenerateMultiplyHighU32);
@ -744,8 +744,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
pCoords = Src(AggregateType.S32);
}
pCoords = ScalingHelpers.ApplyScaling(context, texOp, pCoords, intCoords: true, isBindless, isIndexed, isArray, pCount);
(var imageType, var imageVariable) = context.Images[new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format)];
var image = context.Load(imageType, imageVariable);
@ -1040,6 +1038,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return GenerateBinaryU32(context, operation, context.Delegates.GlslUMin);
}
private static OperationResult GenerateModulo(CodeGenContext context, AstOperation operation)
{
return GenerateBinary(context, operation, context.Delegates.FMod, null);
}
private static OperationResult GenerateMultiply(CodeGenContext context, AstOperation operation)
{
return GenerateBinary(context, operation, context.Delegates.FMul, context.Delegates.IMul);
@ -1101,7 +1104,15 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
private static OperationResult GenerateReturn(CodeGenContext context, AstOperation operation)
{
context.Return();
if (operation.SourcesCount != 0)
{
context.ReturnValue(context.Get(context.CurrentFunction.ReturnType, operation.GetSource(0)));
}
else
{
context.Return();
}
return OperationResult.Invalid;
}
@ -1439,35 +1450,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
}
}
SpvInstruction ApplyBias(SpvInstruction vector, SpvInstruction image)
{
int gatherBiasPrecision = context.Config.GpuAccessor.QueryHostGatherBiasPrecision();
if (isGather && gatherBiasPrecision != 0)
{
// GPU requires texture gather to be slightly offset to match NVIDIA behaviour when point is exactly between two texels.
// Offset by the gather precision divided by 2 to correct for rounding.
var sizeType = pCount == 1 ? context.TypeS32() : context.TypeVector(context.TypeS32(), pCount);
var pVectorType = pCount == 1 ? context.TypeFP32() : context.TypeVector(context.TypeFP32(), pCount);
var bias = context.Constant(context.TypeFP32(), (float)(1 << (gatherBiasPrecision + 1)));
var biasVector = context.CompositeConstruct(pVectorType, Enumerable.Repeat(bias, pCount).ToArray());
var one = context.Constant(context.TypeFP32(), 1f);
var oneVector = context.CompositeConstruct(pVectorType, Enumerable.Repeat(one, pCount).ToArray());
var divisor = context.FMul(
pVectorType,
context.ConvertSToF(pVectorType, context.ImageQuerySize(sizeType, image)),
biasVector);
vector = context.FAdd(pVectorType, vector, context.FDiv(pVectorType, oneVector, divisor));
}
return vector;
}
SpvInstruction pCoords = AssemblePVector(pCount);
pCoords = ScalingHelpers.ApplyScaling(context, texOp, pCoords, intCoords, isBindless, isIndexed, isArray, pCount);
SpvInstruction AssembleDerivativesVector(int count)
{
@ -1638,8 +1621,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
image = context.Image(imageType, image);
}
pCoords = ApplyBias(pCoords, image);
var operands = operandsList.ToArray();
SpvInstruction result;
@ -1755,11 +1736,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
result = context.CompositeExtract(context.TypeS32(), result, (SpvLiteralInteger)texOp.Index);
}
if (texOp.Index < 2 || (type & SamplerType.Mask) == SamplerType.Texture3D)
{
result = ScalingHelpers.ApplyUnscaling(context, texOp.WithType(type), result, isBindless, isIndexed);
}
return new OperationResult(AggregateType.S32, result);
}
}
@ -2269,7 +2245,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
var result = emitF(context.TypeFP64(), context.GetFP64(src1), context.GetFP64(src2));
if (!context.Config.GpuAccessor.QueryHostReducedPrecision())
if (!context.Config.GpuAccessor.QueryHostReducedPrecision() || operation.ForcePrecise)
{
context.Decorate(result, Decoration.NoContraction);
}
@ -2280,7 +2256,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
var result = emitF(context.TypeFP32(), context.GetFP32(src1), context.GetFP32(src2));
if (!context.Config.GpuAccessor.QueryHostReducedPrecision())
if (!context.Config.GpuAccessor.QueryHostReducedPrecision() || operation.ForcePrecise)
{
context.Decorate(result, Decoration.NoContraction);
}
@ -2340,7 +2316,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
var result = emitF(context.TypeFP64(), context.GetFP64(src1), context.GetFP64(src2), context.GetFP64(src3));
if (!context.Config.GpuAccessor.QueryHostReducedPrecision())
if (!context.Config.GpuAccessor.QueryHostReducedPrecision() || operation.ForcePrecise)
{
context.Decorate(result, Decoration.NoContraction);
}
@ -2351,7 +2327,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
var result = emitF(context.TypeFP32(), context.GetFP32(src1), context.GetFP32(src2), context.GetFP32(src3));
if (!context.Config.GpuAccessor.QueryHostReducedPrecision())
if (!context.Config.GpuAccessor.QueryHostReducedPrecision() || operation.ForcePrecise)
{
context.Decorate(result, Decoration.NoContraction);
}

View File

@ -1,227 +0,0 @@
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using Ryujinx.Graphics.Shader.StructuredIr;
using Ryujinx.Graphics.Shader.Translation;
using static Spv.Specification;
namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
using SpvInstruction = Spv.Generator.Instruction;
static class ScalingHelpers
{
public static SpvInstruction ApplyScaling(
CodeGenContext context,
AstTextureOperation texOp,
SpvInstruction vector,
bool intCoords,
bool isBindless,
bool isIndexed,
bool isArray,
int pCount)
{
if (intCoords)
{
if (context.Config.Stage.SupportsRenderScale() &&
!isBindless &&
!isIndexed)
{
int index = texOp.Inst == Instruction.ImageLoad
? context.Config.GetTextureDescriptors().Length + context.Config.FindImageDescriptorIndex(texOp)
: context.Config.FindTextureDescriptorIndex(texOp);
if (pCount == 3 && isArray)
{
return ApplyScaling2DArray(context, vector, index);
}
else if (pCount == 2 && !isArray)
{
return ApplyScaling2D(context, vector, index);
}
}
}
return vector;
}
private static SpvInstruction ApplyScaling2DArray(CodeGenContext context, SpvInstruction vector, int index)
{
// The array index is not scaled, just x and y.
var vectorXY = context.VectorShuffle(context.TypeVector(context.TypeS32(), 2), vector, vector, 0, 1);
var vectorZ = context.CompositeExtract(context.TypeS32(), vector, 2);
var vectorXYScaled = ApplyScaling2D(context, vectorXY, index);
var vectorScaled = context.CompositeConstruct(context.TypeVector(context.TypeS32(), 3), vectorXYScaled, vectorZ);
return vectorScaled;
}
private static SpvInstruction ApplyScaling2D(CodeGenContext context, SpvInstruction vector, int index)
{
var pointerType = context.TypePointer(StorageClass.Uniform, context.TypeFP32());
var fieldIndex = context.Constant(context.TypeU32(), 4);
var scaleIndex = context.Constant(context.TypeU32(), index);
if (context.Config.Stage == ShaderStage.Vertex)
{
var scaleCountPointerType = context.TypePointer(StorageClass.Uniform, context.TypeS32());
var scaleCountElemPointer = context.AccessChain(scaleCountPointerType, context.ConstantBuffers[0], context.Constant(context.TypeU32(), 3));
var scaleCount = context.Load(context.TypeS32(), scaleCountElemPointer);
scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, scaleCount);
}
scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, context.Constant(context.TypeU32(), 1));
var scaleElemPointer = context.AccessChain(pointerType, context.ConstantBuffers[0], fieldIndex, scaleIndex);
var scale = context.Load(context.TypeFP32(), scaleElemPointer);
var ivector2Type = context.TypeVector(context.TypeS32(), 2);
var localVector = context.CoordTemp;
var passthrough = context.FOrdEqual(context.TypeBool(), scale, context.Constant(context.TypeFP32(), 1f));
var mergeLabel = context.Label();
if (context.Config.Stage == ShaderStage.Fragment)
{
var scaledInterpolatedLabel = context.Label();
var scaledNoInterpolationLabel = context.Label();
var needsInterpolation = context.FOrdLessThan(context.TypeBool(), scale, context.Constant(context.TypeFP32(), 0f));
context.SelectionMerge(mergeLabel, SelectionControlMask.MaskNone);
context.BranchConditional(needsInterpolation, scaledInterpolatedLabel, scaledNoInterpolationLabel);
// scale < 0.0
context.AddLabel(scaledInterpolatedLabel);
ApplyScalingInterpolated(context, localVector, vector, scale);
context.Branch(mergeLabel);
// scale >= 0.0
context.AddLabel(scaledNoInterpolationLabel);
ApplyScalingNoInterpolation(context, localVector, vector, scale);
context.Branch(mergeLabel);
context.AddLabel(mergeLabel);
var passthroughLabel = context.Label();
var finalMergeLabel = context.Label();
context.SelectionMerge(finalMergeLabel, SelectionControlMask.MaskNone);
context.BranchConditional(passthrough, passthroughLabel, finalMergeLabel);
context.AddLabel(passthroughLabel);
context.Store(localVector, vector);
context.Branch(finalMergeLabel);
context.AddLabel(finalMergeLabel);
return context.Load(ivector2Type, localVector);
}
else
{
var passthroughLabel = context.Label();
var scaledLabel = context.Label();
context.SelectionMerge(mergeLabel, SelectionControlMask.MaskNone);
context.BranchConditional(passthrough, passthroughLabel, scaledLabel);
// scale == 1.0
context.AddLabel(passthroughLabel);
context.Store(localVector, vector);
context.Branch(mergeLabel);
// scale != 1.0
context.AddLabel(scaledLabel);
ApplyScalingNoInterpolation(context, localVector, vector, scale);
context.Branch(mergeLabel);
context.AddLabel(mergeLabel);
return context.Load(ivector2Type, localVector);
}
}
private static void ApplyScalingInterpolated(CodeGenContext context, SpvInstruction output, SpvInstruction vector, SpvInstruction scale)
{
var vector2Type = context.TypeVector(context.TypeFP32(), 2);
var scaleNegated = context.FNegate(context.TypeFP32(), scale);
var scaleVector = context.CompositeConstruct(vector2Type, scaleNegated, scaleNegated);
var vectorFloat = context.ConvertSToF(vector2Type, vector);
var vectorScaled = context.VectorTimesScalar(vector2Type, vectorFloat, scaleNegated);
var fragCoordPointer = context.Inputs[new IoDefinition(StorageKind.Input, IoVariable.FragmentCoord)];
var fragCoord = context.Load(context.TypeVector(context.TypeFP32(), 4), fragCoordPointer);
var fragCoordXY = context.VectorShuffle(vector2Type, fragCoord, fragCoord, 0, 1);
var scaleMod = context.FMod(vector2Type, fragCoordXY, scaleVector);
var vectorInterpolated = context.FAdd(vector2Type, vectorScaled, scaleMod);
context.Store(output, context.ConvertFToS(context.TypeVector(context.TypeS32(), 2), vectorInterpolated));
}
private static void ApplyScalingNoInterpolation(CodeGenContext context, SpvInstruction output, SpvInstruction vector, SpvInstruction scale)
{
if (context.Config.Stage == ShaderStage.Vertex)
{
scale = context.GlslFAbs(context.TypeFP32(), scale);
}
var vector2Type = context.TypeVector(context.TypeFP32(), 2);
var vectorFloat = context.ConvertSToF(vector2Type, vector);
var vectorScaled = context.VectorTimesScalar(vector2Type, vectorFloat, scale);
context.Store(output, context.ConvertFToS(context.TypeVector(context.TypeS32(), 2), vectorScaled));
}
public static SpvInstruction ApplyUnscaling(
CodeGenContext context,
AstTextureOperation texOp,
SpvInstruction size,
bool isBindless,
bool isIndexed)
{
if (context.Config.Stage.SupportsRenderScale() &&
!isBindless &&
!isIndexed)
{
int index = context.Config.FindTextureDescriptorIndex(texOp);
var pointerType = context.TypePointer(StorageClass.Uniform, context.TypeFP32());
var fieldIndex = context.Constant(context.TypeU32(), 4);
var scaleIndex = context.Constant(context.TypeU32(), index);
if (context.Config.Stage == ShaderStage.Vertex)
{
var scaleCountPointerType = context.TypePointer(StorageClass.Uniform, context.TypeS32());
var scaleCountElemPointer = context.AccessChain(scaleCountPointerType, context.ConstantBuffers[0], context.Constant(context.TypeU32(), 3));
var scaleCount = context.Load(context.TypeS32(), scaleCountElemPointer);
scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, scaleCount);
}
scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, context.Constant(context.TypeU32(), 1));
var scaleElemPointer = context.AccessChain(pointerType, context.ConstantBuffers[0], fieldIndex, scaleIndex);
var scale = context.GlslFAbs(context.TypeFP32(), context.Load(context.TypeFP32(), scaleElemPointer));
var passthrough = context.FOrdEqual(context.TypeBool(), scale, context.Constant(context.TypeFP32(), 1f));
var sizeFloat = context.ConvertSToF(context.TypeFP32(), size);
var sizeUnscaled = context.FDiv(context.TypeFP32(), sizeFloat, scale);
var sizeUnscaledInt = context.ConvertFToS(context.TypeS32(), sizeUnscaled);
return context.Select(context.TypeS32(), passthrough, size, sizeUnscaledInt);
}
return size;
}
}
}

View File

@ -67,6 +67,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
public readonly FuncBinaryInstruction GlslSMax;
public readonly FuncBinaryInstruction GlslFMin;
public readonly FuncBinaryInstruction GlslSMin;
public readonly FuncBinaryInstruction FMod;
public readonly FuncBinaryInstruction FMul;
public readonly FuncBinaryInstruction IMul;
public readonly FuncBinaryInstruction FSub;
@ -174,6 +175,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
GlslSMax = context.GlslSMax;
GlslFMin = context.GlslFMin;
GlslSMin = context.GlslSMin;
FMod = context.FMod;
FMul = context.FMul;
IMul = context.IMul;
FSub = context.FSub;

View File

@ -144,10 +144,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
private static void Generate(CodeGenContext context, StructuredProgramInfo info, int funcIndex)
{
var function = info.Functions[funcIndex];
(_, var spvFunc) = context.GetFunction(funcIndex);
(var function, var spvFunc) = context.GetFunction(funcIndex);
context.CurrentFunction = function;
context.AddFunction(spvFunc);
context.StartFunction();

View File

@ -367,6 +367,15 @@ namespace Ryujinx.Graphics.Shader
return true;
}
/// <summary>
/// Queries whether the host supports depth clip control.
/// </summary>
/// <returns>True if the GPU and driver supports depth clip control, false otherwise</returns>
bool QueryHostSupportsDepthClipControl()
{
return true;
}
/// <summary>
/// Queries the point size from the GPU state, used when it is not explicitly set on the shader.
/// </summary>

View File

@ -187,27 +187,6 @@ namespace Ryujinx.Graphics.Shader.Instructions
context.Config.GpuAccessor.Log("Shader instruction Longjmp is not implemented.");
}
public static void P2rR(EmitterContext context)
{
InstP2rR op = context.GetOp<InstP2rR>();
context.Config.GpuAccessor.Log("Shader instruction P2rR is not implemented.");
}
public static void P2rI(EmitterContext context)
{
InstP2rI op = context.GetOp<InstP2rI>();
context.Config.GpuAccessor.Log("Shader instruction P2rI is not implemented.");
}
public static void P2rC(EmitterContext context)
{
InstP2rC op = context.GetOp<InstP2rC>();
context.Config.GpuAccessor.Log("Shader instruction P2rC is not implemented.");
}
public static void Pexit(EmitterContext context)
{
InstPexit op = context.GetOp<InstPexit>();

View File

@ -209,21 +209,15 @@ namespace Ryujinx.Graphics.Shader.Instructions
return context.ICompareNotEqual(context.BitwiseAnd(value, Const(1 << bit)), Const(0));
}
if (ccpr)
int count = ccpr ? RegisterConsts.FlagsCount : RegisterConsts.PredsCount;
RegisterType type = ccpr ? RegisterType.Flag : RegisterType.Predicate;
int shift = (int)byteSel * 8;
for (int bit = 0; bit < count; bit++)
{
// TODO: Support Register to condition code flags copy.
context.Config.GpuAccessor.Log("R2P.CC not implemented.");
}
else
{
int shift = (int)byteSel * 8;
for (int bit = 0; bit < RegisterConsts.PredsCount; bit++)
{
Operand pred = Register(bit, RegisterType.Predicate);
Operand res = context.ConditionalSelect(Test(mask, bit), Test(value, bit + shift), pred);
context.Copy(pred, res);
}
Operand flag = Register(bit, type);
Operand res = context.ConditionalSelect(Test(mask, bit), Test(value, bit + shift), flag);
context.Copy(flag, res);
}
}

View File

@ -1,7 +1,6 @@
using Ryujinx.Graphics.Shader.Decoders;
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using Ryujinx.Graphics.Shader.Translation;
using static Ryujinx.Graphics.Shader.Instructions.InstEmitAluHelper;
using static Ryujinx.Graphics.Shader.Instructions.InstEmitHelper;
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
@ -50,5 +49,68 @@ namespace Ryujinx.Graphics.Shader.Instructions
context.Copy(Register(op.DestPred, RegisterType.Predicate), p0Res);
context.Copy(Register(op.DestPredInv, RegisterType.Predicate), p1Res);
}
public static void P2rC(EmitterContext context)
{
InstP2rC op = context.GetOp<InstP2rC>();
Operand srcA = GetSrcReg(context, op.SrcA);
Operand dest = GetSrcReg(context, op.Dest);
Operand mask = GetSrcCbuf(context, op.CbufSlot, op.CbufOffset);
EmitP2r(context, srcA, dest, mask, op.ByteSel, op.Ccpr);
}
public static void P2rI(EmitterContext context)
{
InstP2rI op = context.GetOp<InstP2rI>();
Operand srcA = GetSrcReg(context, op.SrcA);
Operand dest = GetSrcReg(context, op.Dest);
Operand mask = GetSrcImm(context, op.Imm20);
EmitP2r(context, srcA, dest, mask, op.ByteSel, op.Ccpr);
}
public static void P2rR(EmitterContext context)
{
InstP2rR op = context.GetOp<InstP2rR>();
Operand srcA = GetSrcReg(context, op.SrcA);
Operand dest = GetSrcReg(context, op.Dest);
Operand mask = GetSrcReg(context, op.SrcB);
EmitP2r(context, srcA, dest, mask, op.ByteSel, op.Ccpr);
}
private static void EmitP2r(
EmitterContext context,
Operand srcA,
Operand dest,
Operand mask,
ByteSel byteSel,
bool ccpr)
{
int count = ccpr ? RegisterConsts.FlagsCount : RegisterConsts.PredsCount;
int shift = (int)byteSel * 8;
mask = context.BitwiseAnd(mask, Const(0xff));
Operand insert = Const(0);
for (int i = 0; i < count; i++)
{
Operand condition = ccpr
? Register(i, RegisterType.Flag)
: Register(i, RegisterType.Predicate);
Operand bit = context.ConditionalSelect(condition, Const(1 << (i + shift)), Const(0));
insert = context.BitwiseOr(insert, bit);
}
Operand maskShifted = context.ShiftLeft(mask, Const(shift));
Operand masked = context.BitwiseAnd(srcA, context.BitwiseNot(maskShifted));
Operand res = context.BitwiseOr(masked, context.BitwiseAnd(insert, maskShifted));
context.Copy(dest, res);
}
}
}

View File

@ -97,6 +97,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
MemoryBarrier,
Minimum,
MinimumU32,
Modulo,
Multiply,
MultiplyHighS32,
MultiplyHighU32,

View File

@ -8,6 +8,8 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
public Instruction Inst { get; private set; }
public StorageKind StorageKind { get; }
public bool ForcePrecise { get; set; }
private Operand[] _dests;
public Operand Dest

View File

@ -4,10 +4,6 @@
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="CodeGen\Glsl\HelperFunctions\TexelFetchScale_vp.glsl" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Spv.Generator\Spv.Generator.csproj" />
@ -25,9 +21,6 @@
<EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\StoreSharedSmallInt.glsl" />
<EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\StoreStorageSmallInt.glsl" />
<EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\SwizzleAdd.glsl" />
<EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\TexelFetchScale_vp.glsl" />
<EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\TexelFetchScale_fp.glsl" />
<EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\TexelFetchScale_cp.glsl" />
</ItemGroup>
</Project>

View File

@ -10,6 +10,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
{
public Instruction Inst { get; }
public StorageKind StorageKind { get; }
public bool ForcePrecise { get; }
public int Index { get; }
@ -17,10 +18,11 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
public int SourcesCount => _sources.Length;
public AstOperation(Instruction inst, StorageKind storageKind, IAstNode[] sources, int sourcesCount)
public AstOperation(Instruction inst, StorageKind storageKind, bool forcePrecise, IAstNode[] sources, int sourcesCount)
{
Inst = inst;
StorageKind = storageKind;
ForcePrecise = forcePrecise;
_sources = sources;
for (int index = 0; index < sources.Length; index++)
@ -38,12 +40,18 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
Index = 0;
}
public AstOperation(Instruction inst, StorageKind storageKind, int index, IAstNode[] sources, int sourcesCount) : this(inst, storageKind, sources, sourcesCount)
public AstOperation(
Instruction inst,
StorageKind storageKind,
bool forcePrecise,
int index,
IAstNode[] sources,
int sourcesCount) : this(inst, storageKind, forcePrecise, sources, sourcesCount)
{
Index = index;
}
public AstOperation(Instruction inst, params IAstNode[] sources) : this(inst, StorageKind.None, sources, sources.Length)
public AstOperation(Instruction inst, params IAstNode[] sources) : this(inst, StorageKind.None, false, sources, sources.Length)
{
}

View File

@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
int cbufSlot,
int handle,
int index,
params IAstNode[] sources) : base(inst, StorageKind.None, index, sources, sources.Length)
params IAstNode[] sources) : base(inst, StorageKind.None, false, index, sources, sources.Length)
{
Type = type;
Format = format;
@ -27,10 +27,5 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
CbufSlot = cbufSlot;
Handle = handle;
}
public AstTextureOperation WithType(SamplerType type)
{
return new AstTextureOperation(Inst, type, Format, Flags, CbufSlot, Handle, Index);
}
}
}

View File

@ -104,6 +104,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
Add(Instruction.MaximumU32, AggregateType.U32, AggregateType.U32, AggregateType.U32);
Add(Instruction.Minimum, AggregateType.Scalar, AggregateType.Scalar, AggregateType.Scalar);
Add(Instruction.MinimumU32, AggregateType.U32, AggregateType.U32, AggregateType.U32);
Add(Instruction.Modulo, AggregateType.Scalar, AggregateType.Scalar, AggregateType.Scalar);
Add(Instruction.Multiply, AggregateType.Scalar, AggregateType.Scalar, AggregateType.Scalar);
Add(Instruction.MultiplyHighS32, AggregateType.S32, AggregateType.S32, AggregateType.S32);
Add(Instruction.MultiplyHighU32, AggregateType.U32, AggregateType.U32, AggregateType.U32);

View File

@ -8,11 +8,11 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
{
static class StructuredProgram
{
public static StructuredProgramInfo MakeStructuredProgram(Function[] functions, ShaderConfig config)
public static StructuredProgramInfo MakeStructuredProgram(IReadOnlyList<Function> functions, ShaderConfig config)
{
StructuredProgramContext context = new StructuredProgramContext(config);
for (int funcIndex = 0; funcIndex < functions.Length; funcIndex++)
for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++)
{
Function function = functions[funcIndex];
@ -156,7 +156,13 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
}
else
{
source = new AstOperation(inst, operation.StorageKind, operation.Index, sources, operation.SourcesCount);
source = new AstOperation(
inst,
operation.StorageKind,
operation.ForcePrecise,
operation.Index,
sources,
operation.SourcesCount);
}
AggregateType destElemType = destType;
@ -179,7 +185,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
dest.VarType = destElemType;
context.AddNode(new AstAssignment(dest, new AstOperation(Instruction.VectorExtract, StorageKind.None, new[] { destVec, index }, 2)));
context.AddNode(new AstAssignment(dest, new AstOperation(Instruction.VectorExtract, StorageKind.None, false, new[] { destVec, index }, 2)));
}
}
else if (operation.Dest != null)
@ -227,7 +233,13 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
}
else if (!isCopy)
{
source = new AstOperation(inst, operation.StorageKind, operation.Index, sources, operation.SourcesCount);
source = new AstOperation(
inst,
operation.StorageKind,
operation.ForcePrecise,
operation.Index,
sources,
operation.SourcesCount);
}
else
{
@ -248,7 +260,13 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
}
else
{
context.AddNode(new AstOperation(inst, operation.StorageKind, operation.Index, sources, operation.SourcesCount));
context.AddNode(new AstOperation(
inst,
operation.StorageKind,
operation.ForcePrecise,
operation.Index,
sources,
operation.SourcesCount));
}
// Those instructions needs to be emulated by using helper functions,

View File

@ -319,7 +319,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
new AstOperand(OperandType.Constant, elemIndex)
};
return new AstOperation(Instruction.Load, StorageKind.ConstantBuffer, sources, sources.Length);
return new AstOperation(Instruction.Load, StorageKind.ConstantBuffer, false, sources, sources.Length);
}
return GetOperand(operand);

View File

@ -49,13 +49,17 @@ namespace Ryujinx.Graphics.Shader.Translation
private readonly List<Operation> _operations;
private readonly Dictionary<ulong, BlockLabel> _labels;
public EmitterContext(DecodedProgram program, ShaderConfig config, bool isNonMain)
public EmitterContext()
{
_operations = new List<Operation>();
_labels = new Dictionary<ulong, BlockLabel>();
}
public EmitterContext(DecodedProgram program, ShaderConfig config, bool isNonMain) : this()
{
Program = program;
Config = config;
IsNonMain = isNonMain;
_operations = new List<Operation>();
_labels = new Dictionary<ulong, BlockLabel>();
EmitStart();
}
@ -242,7 +246,7 @@ namespace Ryujinx.Graphics.Shader.Translation
this.Store(StorageKind.Output, IoVariable.Position, null, Const(1), this.FPFusedMultiplyAdd(y, yScale, negativeOne));
}
if (Config.Options.TargetApi == TargetApi.Vulkan && Config.GpuAccessor.QueryTransformDepthMinusOneToOne())
if (Config.GpuAccessor.QueryTransformDepthMinusOneToOne() && !Config.GpuAccessor.QueryHostSupportsDepthClipControl())
{
Operand z = this.Load(StorageKind.Output, IoVariable.Position, null, Const(2));
Operand w = this.Load(StorageKind.Output, IoVariable.Position, null, Const(3));
@ -279,7 +283,7 @@ namespace Ryujinx.Graphics.Shader.Translation
oldYLocal = null;
}
if (Config.Options.TargetApi == TargetApi.Vulkan && Config.GpuAccessor.QueryTransformDepthMinusOneToOne())
if (Config.GpuAccessor.QueryTransformDepthMinusOneToOne() && !Config.GpuAccessor.QueryHostSupportsDepthClipControl())
{
oldZLocal = Local();
this.Copy(oldZLocal, this.Load(StorageKind.Output, IoVariable.Position, null, Const(2)));

View File

@ -307,6 +307,11 @@ namespace Ryujinx.Graphics.Shader.Translation
return context.Add(fpType | Instruction.Minimum, Local(), a, b);
}
public static Operand FPModulo(this EmitterContext context, Operand a, Operand b, Instruction fpType = Instruction.FP32)
{
return context.Add(fpType | Instruction.Modulo, Local(), a, b);
}
public static Operand FPMultiply(this EmitterContext context, Operand a, Operand b, Instruction fpType = Instruction.FP32)
{
return context.Add(fpType | Instruction.Multiply, Local(), a, b);
@ -656,7 +661,6 @@ namespace Ryujinx.Graphics.Shader.Translation
public static void Return(this EmitterContext context, Operand returnValue)
{
context.PrepareForReturn();
context.Add(Instruction.Return, null, returnValue);
}

View File

@ -0,0 +1,134 @@
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using System;
using System.Collections.Generic;
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
namespace Ryujinx.Graphics.Shader.Translation
{
class HelperFunctionManager
{
private readonly List<Function> _functionList;
private readonly Dictionary<HelperFunctionName, int> _functionIds;
private readonly ShaderStage _stage;
public HelperFunctionManager(List<Function> functionList, ShaderStage stage)
{
_functionList = functionList;
_functionIds = new Dictionary<HelperFunctionName, int>();
_stage = stage;
}
public int GetOrCreateFunctionId(HelperFunctionName functionName)
{
if (_functionIds.TryGetValue(functionName, out int functionId))
{
return functionId;
}
Function function = GenerateFunction(functionName);
functionId = _functionList.Count;
_functionList.Add(function);
_functionIds.Add(functionName, functionId);
return functionId;
}
private Function GenerateFunction(HelperFunctionName functionName)
{
return functionName switch
{
HelperFunctionName.TexelFetchScale => GenerateTexelFetchScaleFunction(),
HelperFunctionName.TextureSizeUnscale => GenerateTextureSizeUnscaleFunction(),
_ => throw new ArgumentException($"Invalid function name {functionName}")
};
}
private Function GenerateTexelFetchScaleFunction()
{
EmitterContext context = new EmitterContext();
Operand input = Argument(0);
Operand samplerIndex = Argument(1);
Operand index = GetScaleIndex(context, samplerIndex);
Operand scale = context.Load(StorageKind.ConstantBuffer, 0, Const((int)SupportBufferField.RenderScale), index);
Operand scaleIsOne = context.FPCompareEqual(scale, ConstF(1f));
Operand lblScaleNotOne = Label();
context.BranchIfFalse(lblScaleNotOne, scaleIsOne);
context.Return(input);
context.MarkLabel(lblScaleNotOne);
int inArgumentsCount;
if (_stage == ShaderStage.Fragment)
{
Operand scaleIsLessThanZero = context.FPCompareLess(scale, ConstF(0f));
Operand lblScaleGreaterOrEqualZero = Label();
context.BranchIfFalse(lblScaleGreaterOrEqualZero, scaleIsLessThanZero);
Operand negScale = context.FPNegate(scale);
Operand inputScaled = context.FPMultiply(context.IConvertS32ToFP32(input), negScale);
Operand fragCoordX = context.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(0));
Operand fragCoordY = context.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(1));
Operand fragCoord = context.ConditionalSelect(Argument(2), fragCoordY, fragCoordX);
Operand inputBias = context.FPModulo(fragCoord, negScale);
Operand inputWithBias = context.FPAdd(inputScaled, inputBias);
context.Return(context.FP32ConvertToS32(inputWithBias));
context.MarkLabel(lblScaleGreaterOrEqualZero);
inArgumentsCount = 3;
}
else
{
inArgumentsCount = 2;
}
Operand inputScaled2 = context.FPMultiply(context.IConvertS32ToFP32(input), scale);
context.Return(context.FP32ConvertToS32(inputScaled2));
return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "TexelFetchScale", true, inArgumentsCount, 0);
}
private Function GenerateTextureSizeUnscaleFunction()
{
EmitterContext context = new EmitterContext();
Operand input = Argument(0);
Operand samplerIndex = Argument(1);
Operand index = GetScaleIndex(context, samplerIndex);
Operand scale = context.FPAbsolute(context.Load(StorageKind.ConstantBuffer, 0, Const((int)SupportBufferField.RenderScale), index));
Operand scaleIsOne = context.FPCompareEqual(scale, ConstF(1f));
Operand lblScaleNotOne = Label();
context.BranchIfFalse(lblScaleNotOne, scaleIsOne);
context.Return(input);
context.MarkLabel(lblScaleNotOne);
Operand inputUnscaled = context.FPDivide(context.IConvertS32ToFP32(input), scale);
context.Return(context.FP32ConvertToS32(inputUnscaled));
return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "TextureSizeUnscale", true, 2, 0);
}
private Operand GetScaleIndex(EmitterContext context, Operand index)
{
switch (_stage)
{
case ShaderStage.Vertex:
Operand fragScaleCount = context.Load(StorageKind.ConstantBuffer, 0, Const((int)SupportBufferField.FragmentRenderScaleCount));
return context.IAdd(Const(1), context.IAdd(index, fragScaleCount));
default:
return context.IAdd(Const(1), index);
}
}
}
}

View File

@ -0,0 +1,11 @@
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Shader.Translation
{
enum HelperFunctionName
{
TexelFetchScale,
TextureSizeUnscale
}
}

View File

@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Shader.Translation
private readonly HashSet<int> _usedConstantBufferBindings;
public ShaderProperties Properties => _properties;
public ResourceManager(ShaderStage stage, IGpuAccessor gpuAccessor, ShaderProperties properties)
{
_gpuAccessor = gpuAccessor;
@ -98,19 +100,6 @@ namespace Ryujinx.Graphics.Shader.Translation
_properties.AddConstantBuffer(binding, new BufferDefinition(BufferLayout.Std140, 0, binding, name, type));
}
public void InheritFrom(ResourceManager other)
{
for (int i = 0; i < other._cbSlotToBindingMap.Length; i++)
{
int binding = other._cbSlotToBindingMap[i];
if (binding >= 0)
{
_cbSlotToBindingMap[i] = binding;
}
}
}
public static string GetShaderStagePrefix(ShaderStage stage)
{
uint index = (uint)stage;

View File

@ -11,9 +11,10 @@ namespace Ryujinx.Graphics.Shader.Translation
{
static class Rewriter
{
public static void RunPass(BasicBlock[] blocks, ShaderConfig config)
public static void RunPass(HelperFunctionManager hfm, BasicBlock[] blocks, ShaderConfig config)
{
bool isVertexShader = config.Stage == ShaderStage.Vertex;
bool isImpreciseFragmentShader = config.Stage == ShaderStage.Fragment && config.GpuAccessor.QueryHostReducedPrecision();
bool hasConstantBufferDrawParameters = config.GpuAccessor.QueryHasConstantBufferDrawParameters();
bool hasVectorIndexingBug = config.GpuAccessor.QueryHostHasVectorIndexingBug();
bool supportsSnormBufferTextureFormat = config.GpuAccessor.QueryHostSupportsSnormBufferTextureFormat();
@ -45,6 +46,11 @@ namespace Ryujinx.Graphics.Shader.Translation
}
}
if (isImpreciseFragmentShader)
{
EnableForcePreciseIfNeeded(operation);
}
if (hasVectorIndexingBug)
{
InsertVectorComponentSelect(node, config);
@ -54,9 +60,14 @@ namespace Ryujinx.Graphics.Shader.Translation
if (operation is TextureOperation texOp)
{
node = InsertTexelFetchScale(hfm, node, config);
node = InsertTextureSizeUnscale(hfm, node, config);
if (texOp.Inst == Instruction.TextureSample)
{
node = RewriteTextureSample(node, config);
node = InsertCoordNormalization(node, config);
node = InsertCoordGatherBias(node, config);
node = InsertConstOffsets(node, config);
if (texOp.Type == SamplerType.TextureBuffer && !supportsSnormBufferTextureFormat)
{
@ -76,6 +87,25 @@ namespace Ryujinx.Graphics.Shader.Translation
}
}
private static void EnableForcePreciseIfNeeded(Operation operation)
{
// There are some cases where a small bias is added to values to prevent division by zero.
// When operating with reduced precision, it is possible for this bias to get rounded to 0
// and cause a division by zero.
// To prevent that, we force those operations to be precise even if the host wants
// imprecise operations for performance.
if (operation.Inst == (Instruction.FP32 | Instruction.Divide) &&
operation.GetSource(0).Type == OperandType.Constant &&
operation.GetSource(0).AsFloat() == 1f &&
operation.GetSource(1).AsgOp is Operation addOp &&
addOp.Inst == (Instruction.FP32 | Instruction.Add) &&
addOp.GetSource(1).Type == OperandType.Constant)
{
addOp.ForcePrecise = true;
}
}
private static void InsertVectorComponentSelect(LinkedListNode<INode> node, ShaderConfig config)
{
Operation operation = (Operation)node.Value;
@ -344,10 +374,279 @@ namespace Ryujinx.Graphics.Shader.Translation
return node;
}
private static LinkedListNode<INode> RewriteTextureSample(LinkedListNode<INode> node, ShaderConfig config)
private static LinkedListNode<INode> InsertTexelFetchScale(HelperFunctionManager hfm, LinkedListNode<INode> node, ShaderConfig config)
{
TextureOperation texOp = (TextureOperation)node.Value;
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
bool isArray = (texOp.Type & SamplerType.Array) != 0;
bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
int coordsCount = texOp.Type.GetDimensions();
int coordsIndex = isBindless || isIndexed ? 1 : 0;
bool isImage = IsImageInstructionWithScale(texOp.Inst);
if ((texOp.Inst == Instruction.TextureSample || isImage) &&
(intCoords || isImage) &&
!isBindless &&
!isIndexed &&
config.Stage.SupportsRenderScale() &&
TypeSupportsScale(texOp.Type))
{
int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TexelFetchScale);
int samplerIndex = isImage
? config.GetTextureDescriptors().Length + config.FindImageDescriptorIndex(texOp)
: config.FindTextureDescriptorIndex(texOp);
for (int index = 0; index < coordsCount; index++)
{
Operand scaledCoord = Local();
Operand[] callArgs;
if (config.Stage == ShaderStage.Fragment)
{
callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), Const(samplerIndex), Const(index) };
}
else
{
callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), Const(samplerIndex) };
}
node.List.AddBefore(node, new Operation(Instruction.Call, 0, scaledCoord, callArgs));
texOp.SetSource(coordsIndex + index, scaledCoord);
}
}
return node;
}
private static LinkedListNode<INode> InsertTextureSizeUnscale(HelperFunctionManager hfm, LinkedListNode<INode> node, ShaderConfig config)
{
TextureOperation texOp = (TextureOperation)node.Value;
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
bool isArray = (texOp.Type & SamplerType.Array) != 0;
bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
if (texOp.Inst == Instruction.TextureSize &&
texOp.Index < 2 &&
!isBindless &&
!isIndexed &&
config.Stage.SupportsRenderScale() &&
TypeSupportsScale(texOp.Type))
{
int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TextureSizeUnscale);
int samplerIndex = config.FindTextureDescriptorIndex(texOp, ignoreType: true);
for (int index = texOp.DestsCount - 1; index >= 0; index--)
{
Operand dest = texOp.GetDest(index);
Operand unscaledSize = Local();
// Replace all uses with the unscaled size value.
// This must be done before the call is added, since it also is a use of the original size.
foreach (INode useOp in dest.UseOps)
{
for (int srcIndex = 0; srcIndex < useOp.SourcesCount; srcIndex++)
{
if (useOp.GetSource(srcIndex) == dest)
{
useOp.SetSource(srcIndex, unscaledSize);
}
}
}
Operand[] callArgs = new Operand[] { Const(functionId), dest, Const(samplerIndex) };
node.List.AddAfter(node, new Operation(Instruction.Call, 0, unscaledSize, callArgs));
}
}
return node;
}
private static bool IsImageInstructionWithScale(Instruction inst)
{
// Currently, we don't support scaling images that are modified,
// so we only need to care about the load instruction.
return inst == Instruction.ImageLoad;
}
private static bool TypeSupportsScale(SamplerType type)
{
return (type & SamplerType.Mask) == SamplerType.Texture2D;
}
private static LinkedListNode<INode> InsertCoordNormalization(LinkedListNode<INode> node, ShaderConfig config)
{
// Emulate non-normalized coordinates by normalizing the coordinates on the shader.
// Without normalization, the coordinates are expected to the in the [0, W or H] range,
// and otherwise, it is expected to be in the [0, 1] range.
// We normalize by dividing the coords by the texture size.
TextureOperation texOp = (TextureOperation)node.Value;
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
bool isCoordNormalized = isBindless || config.GpuAccessor.QueryTextureCoordNormalized(texOp.Handle, texOp.CbufSlot);
if (isCoordNormalized || intCoords)
{
return node;
}
bool isArray = (texOp.Type & SamplerType.Array) != 0;
bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
int coordsCount = texOp.Type.GetDimensions();
int coordsIndex = isBindless || isIndexed ? 1 : 0;
config.SetUsedFeature(FeatureFlags.IntegerSampling);
int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
for (int index = 0; index < normCoordsCount; index++)
{
Operand coordSize = Local();
Operand[] texSizeSources;
if (isBindless || isIndexed)
{
texSizeSources = new Operand[] { texOp.GetSource(0), Const(0) };
}
else
{
texSizeSources = new Operand[] { Const(0) };
}
node.List.AddBefore(node, new TextureOperation(
Instruction.TextureSize,
texOp.Type,
texOp.Format,
texOp.Flags,
texOp.CbufSlot,
texOp.Handle,
index,
new[] { coordSize },
texSizeSources));
config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
Operand source = texOp.GetSource(coordsIndex + index);
Operand coordNormalized = Local();
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, coordNormalized, source, GenerateI2f(node, coordSize)));
texOp.SetSource(coordsIndex + index, coordNormalized);
}
return node;
}
private static LinkedListNode<INode> InsertCoordGatherBias(LinkedListNode<INode> node, ShaderConfig config)
{
// The gather behavior when the coordinate sits right in the middle of two texels is not well defined.
// To ensure the correct texel is sampled, we add a small bias value to the coordinate.
// This value is calculated as the minimum value required to change the texel it will sample from,
// and is 0 if the host does not require the bias.
TextureOperation texOp = (TextureOperation)node.Value;
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
bool isGather = (texOp.Flags & TextureFlags.Gather) != 0;
int gatherBiasPrecision = config.GpuAccessor.QueryHostGatherBiasPrecision();
if (!isGather || gatherBiasPrecision == 0)
{
return node;
}
bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
bool isArray = (texOp.Type & SamplerType.Array) != 0;
bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
int coordsCount = texOp.Type.GetDimensions();
int coordsIndex = isBindless || isIndexed ? 1 : 0;
config.SetUsedFeature(FeatureFlags.IntegerSampling);
int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
for (int index = 0; index < normCoordsCount; index++)
{
Operand coordSize = Local();
Operand scaledSize = Local();
Operand bias = Local();
Operand[] texSizeSources;
if (isBindless || isIndexed)
{
texSizeSources = new Operand[] { texOp.GetSource(0), Const(0) };
}
else
{
texSizeSources = new Operand[] { Const(0) };
}
node.List.AddBefore(node, new TextureOperation(
Instruction.TextureSize,
texOp.Type,
texOp.Format,
texOp.Flags,
texOp.CbufSlot,
texOp.Handle,
index,
new[] { coordSize },
texSizeSources));
config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
node.List.AddBefore(node, new Operation(
Instruction.FP32 | Instruction.Multiply,
scaledSize,
GenerateI2f(node, coordSize),
ConstF((float)(1 << (gatherBiasPrecision + 1)))));
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, bias, ConstF(1f), scaledSize));
Operand source = texOp.GetSource(coordsIndex + index);
Operand coordBiased = Local();
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Add, coordBiased, source, bias));
texOp.SetSource(coordsIndex + index, coordBiased);
}
return node;
}
private static LinkedListNode<INode> InsertConstOffsets(LinkedListNode<INode> node, ShaderConfig config)
{
// Non-constant texture offsets are not allowed (according to the spec),
// however some GPUs does support that.
// For GPUs where it is not supported, we can replace the instruction with the following:
// For texture*Offset, we replace it by texture*, and add the offset to the P coords.
// The offset can be calculated as offset / textureSize(lod), where lod = textureQueryLod(coords).
// For texelFetchOffset, we replace it by texelFetch and add the offset to the P coords directly.
// For textureGatherOffset, we split the operation into up to 4 operations, one for each component
// that is accessed, where each textureGather operation has a different offset for each pixel.
TextureOperation texOp = (TextureOperation)node.Value;
bool hasOffset = (texOp.Flags & TextureFlags.Offset) != 0;
bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0;
@ -355,9 +654,7 @@ namespace Ryujinx.Graphics.Shader.Translation
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
bool isCoordNormalized = isBindless || config.GpuAccessor.QueryTextureCoordNormalized(texOp.Handle, texOp.CbufSlot);
if (!hasInvalidOffset && isCoordNormalized)
if (!hasInvalidOffset)
{
return node;
}
@ -454,7 +751,7 @@ namespace Ryujinx.Graphics.Shader.Translation
hasInvalidOffset &= !areAllOffsetsConstant;
if (!hasInvalidOffset && isCoordNormalized)
if (!hasInvalidOffset)
{
return node;
}
@ -473,63 +770,6 @@ namespace Ryujinx.Graphics.Shader.Translation
int componentIndex = texOp.Index;
Operand Float(Operand value)
{
Operand res = Local();
node.List.AddBefore(node, new Operation(Instruction.ConvertS32ToFP32, res, value));
return res;
}
// Emulate non-normalized coordinates by normalizing the coordinates on the shader.
// Without normalization, the coordinates are expected to the in the [0, W or H] range,
// and otherwise, it is expected to be in the [0, 1] range.
// We normalize by dividing the coords by the texture size.
if (!isCoordNormalized && !intCoords)
{
config.SetUsedFeature(FeatureFlags.IntegerSampling);
int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
for (int index = 0; index < normCoordsCount; index++)
{
Operand coordSize = Local();
Operand[] texSizeSources;
if (isBindless || isIndexed)
{
texSizeSources = new Operand[] { sources[0], Const(0) };
}
else
{
texSizeSources = new Operand[] { Const(0) };
}
node.List.AddBefore(node, new TextureOperation(
Instruction.TextureSize,
texOp.Type,
texOp.Format,
texOp.Flags,
texOp.CbufSlot,
texOp.Handle,
index,
new[] { coordSize },
texSizeSources));
config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
Operand source = sources[coordsIndex + index];
Operand coordNormalized = Local();
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, coordNormalized, source, Float(coordSize)));
sources[coordsIndex + index] = coordNormalized;
}
}
Operand[] dests = new Operand[texOp.DestsCount];
for (int i = 0; i < texOp.DestsCount; i++)
@ -541,15 +781,7 @@ namespace Ryujinx.Graphics.Shader.Translation
LinkedListNode<INode> oldNode = node;
// Technically, non-constant texture offsets are not allowed (according to the spec),
// however some GPUs does support that.
// For GPUs where it is not supported, we can replace the instruction with the following:
// For texture*Offset, we replace it by texture*, and add the offset to the P coords.
// The offset can be calculated as offset / textureSize(lod), where lod = textureQueryLod(coords).
// For texelFetchOffset, we replace it by texelFetch and add the offset to the P coords directly.
// For textureGatherOffset, we split the operation into up to 4 operations, one for each component
// that is accessed, where each textureGather operation has a different offset for each pixel.
if (hasInvalidOffset && isGather && !isShadow)
if (isGather && !isShadow)
{
config.SetUsedFeature(FeatureFlags.IntegerSampling);
@ -557,7 +789,7 @@ namespace Ryujinx.Graphics.Shader.Translation
sources.CopyTo(newSources, 0);
Operand[] texSizes = InsertTextureSize(node, texOp, lodSources, bindlessHandle, coordsCount);
Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
int destIndex = 0;
@ -576,7 +808,11 @@ namespace Ryujinx.Graphics.Shader.Translation
Operand intOffset = offsets[index + (hasOffsets ? compIndex * coordsCount : 0)];
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, offset, Float(intOffset), Float(texSizes[index])));
node.List.AddBefore(node, new Operation(
Instruction.FP32 | Instruction.Divide,
offset,
GenerateI2f(node, intOffset),
GenerateI2f(node, texSizes[index])));
Operand source = sources[coordsIndex + index];
@ -603,45 +839,46 @@ namespace Ryujinx.Graphics.Shader.Translation
}
else
{
if (hasInvalidOffset)
if (intCoords)
{
if (intCoords)
for (int index = 0; index < coordsCount; index++)
{
for (int index = 0; index < coordsCount; index++)
{
Operand source = sources[coordsIndex + index];
Operand source = sources[coordsIndex + index];
Operand coordPlusOffset = Local();
Operand coordPlusOffset = Local();
node.List.AddBefore(node, new Operation(Instruction.Add, coordPlusOffset, source, offsets[index]));
node.List.AddBefore(node, new Operation(Instruction.Add, coordPlusOffset, source, offsets[index]));
sources[coordsIndex + index] = coordPlusOffset;
}
sources[coordsIndex + index] = coordPlusOffset;
}
else
}
else
{
config.SetUsedFeature(FeatureFlags.IntegerSampling);
Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
for (int index = 0; index < coordsCount; index++)
{
config.SetUsedFeature(FeatureFlags.IntegerSampling);
config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
Operand[] texSizes = InsertTextureSize(node, texOp, lodSources, bindlessHandle, coordsCount);
Operand offset = Local();
for (int index = 0; index < coordsCount; index++)
{
config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
Operand intOffset = offsets[index];
Operand offset = Local();
node.List.AddBefore(node, new Operation(
Instruction.FP32 | Instruction.Divide,
offset,
GenerateI2f(node, intOffset),
GenerateI2f(node, texSizes[index])));
Operand intOffset = offsets[index];
Operand source = sources[coordsIndex + index];
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, offset, Float(intOffset), Float(texSizes[index])));
Operand coordPlusOffset = Local();
Operand source = sources[coordsIndex + index];
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Add, coordPlusOffset, source, offset));
Operand coordPlusOffset = Local();
node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Add, coordPlusOffset, source, offset));
sources[coordsIndex + index] = coordPlusOffset;
}
sources[coordsIndex + index] = coordPlusOffset;
}
}
@ -669,22 +906,13 @@ namespace Ryujinx.Graphics.Shader.Translation
return node;
}
private static Operand[] InsertTextureSize(
private static Operand[] InsertTextureLod(
LinkedListNode<INode> node,
TextureOperation texOp,
Operand[] lodSources,
Operand bindlessHandle,
int coordsCount)
{
Operand Int(Operand value)
{
Operand res = Local();
node.List.AddBefore(node, new Operation(Instruction.ConvertFP32ToS32, res, value));
return res;
}
Operand[] texSizes = new Operand[coordsCount];
Operand lod = Local();
@ -708,11 +936,11 @@ namespace Ryujinx.Graphics.Shader.Translation
if (bindlessHandle != null)
{
texSizeSources = new Operand[] { bindlessHandle, Int(lod) };
texSizeSources = new Operand[] { bindlessHandle, GenerateF2i(node, lod) };
}
else
{
texSizeSources = new Operand[] { Int(lod) };
texSizeSources = new Operand[] { GenerateF2i(node, lod) };
}
node.List.AddBefore(node, new TextureOperation(
@ -796,6 +1024,24 @@ namespace Ryujinx.Graphics.Shader.Translation
return node;
}
private static Operand GenerateI2f(LinkedListNode<INode> node, Operand value)
{
Operand res = Local();
node.List.AddBefore(node, new Operation(Instruction.ConvertS32ToFP32, res, value));
return res;
}
private static Operand GenerateF2i(LinkedListNode<INode> node, Operand value)
{
Operand res = Local();
node.List.AddBefore(node, new Operation(Instruction.ConvertFP32ToS32, res, value));
return res;
}
private static bool ReplaceConstantBufferWithDrawParameters(LinkedListNode<INode> node, Operation operation)
{
Operand GenerateLoad(IoVariable ioVariable)

View File

@ -39,9 +39,9 @@ namespace Ryujinx.Graphics.Shader.Translation
public TranslationOptions Options { get; }
public ShaderProperties Properties { get; }
public ShaderProperties Properties => ResourceManager.Properties;
public ResourceManager ResourceManager { get; }
public ResourceManager ResourceManager { get; set; }
public bool TransformFeedbackEnabled { get; }
@ -159,8 +159,7 @@ namespace Ryujinx.Graphics.Shader.Translation
_sbSlots = new Dictionary<int, int>();
_sbSlotsReverse = new Dictionary<int, int>();
Properties = new ShaderProperties();
ResourceManager = new ResourceManager(stage, gpuAccessor, Properties);
ResourceManager = new ResourceManager(stage, gpuAccessor, new ShaderProperties());
}
public ShaderConfig(
@ -429,8 +428,6 @@ namespace Ryujinx.Graphics.Shader.Translation
public void InheritFrom(ShaderConfig other)
{
ResourceManager.InheritFrom(other.ResourceManager);
ClipDistancesWritten |= other.ClipDistancesWritten;
UsedFeatures |= other.UsedFeatures;
@ -860,7 +857,7 @@ namespace Ryujinx.Graphics.Shader.Translation
return descriptors;
}
public (TextureDescriptor, int) FindTextureDescriptor(AstTextureOperation texOp)
public TextureDescriptor FindTextureDescriptor(AstTextureOperation texOp)
{
TextureDescriptor[] descriptors = GetTextureDescriptors();
@ -872,11 +869,11 @@ namespace Ryujinx.Graphics.Shader.Translation
descriptor.HandleIndex == texOp.Handle &&
descriptor.Format == texOp.Format)
{
return (descriptor, i);
return descriptor;
}
}
return (default, -1);
return default;
}
private static int FindDescriptorIndex(TextureDescriptor[] array, AstTextureOperation texOp)
@ -897,12 +894,30 @@ namespace Ryujinx.Graphics.Shader.Translation
return -1;
}
public int FindTextureDescriptorIndex(AstTextureOperation texOp)
private static int FindDescriptorIndex(TextureDescriptor[] array, TextureOperation texOp, bool ignoreType = false)
{
return FindDescriptorIndex(GetTextureDescriptors(), texOp);
for (int i = 0; i < array.Length; i++)
{
var descriptor = array[i];
if ((descriptor.Type == texOp.Type || ignoreType) &&
descriptor.CbufSlot == texOp.CbufSlot &&
descriptor.HandleIndex == texOp.Handle &&
descriptor.Format == texOp.Format)
{
return i;
}
}
return -1;
}
public int FindImageDescriptorIndex(AstTextureOperation texOp)
public int FindTextureDescriptorIndex(TextureOperation texOp, bool ignoreType = false)
{
return FindDescriptorIndex(GetTextureDescriptors(), texOp, ignoreType);
}
public int FindImageDescriptorIndex(TextureOperation texOp)
{
return FindDescriptorIndex(GetImageDescriptors(), texOp);
}

View File

@ -1,11 +1,11 @@
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Shader.Translation
{
static class ShaderIdentifier
{
public static ShaderIdentification Identify(Function[] functions, ShaderConfig config)
public static ShaderIdentification Identify(IReadOnlyList<Function> functions, ShaderConfig config)
{
if (config.Stage == ShaderStage.Geometry &&
config.GpuAccessor.QueryPrimitiveTopology() == InputTopology.Triangles &&
@ -20,12 +20,12 @@ namespace Ryujinx.Graphics.Shader.Translation
return ShaderIdentification.None;
}
private static bool IsLayerPassthroughGeometryShader(Function[] functions, out int layerInputAttr)
private static bool IsLayerPassthroughGeometryShader(IReadOnlyList<Function> functions, out int layerInputAttr)
{
bool writesLayer = false;
layerInputAttr = 0;
if (functions.Length != 1)
if (functions.Count != 1)
{
return false;
}

View File

@ -5,6 +5,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using Ryujinx.Graphics.Shader.StructuredIr;
using Ryujinx.Graphics.Shader.Translation.Optimizations;
using System;
using System.Collections.Generic;
using System.Linq;
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
@ -44,7 +45,14 @@ namespace Ryujinx.Graphics.Shader.Translation
}
}
Function[] funcs = new Function[functions.Length];
List<Function> funcs = new List<Function>(functions.Length);
for (int i = 0; i < functions.Length; i++)
{
funcs.Add(null);
}
HelperFunctionManager hfm = new HelperFunctionManager(funcs, config.Stage);
for (int i = 0; i < functions.Length; i++)
{
@ -71,7 +79,7 @@ namespace Ryujinx.Graphics.Shader.Translation
Ssa.Rename(cfg.Blocks);
Optimizer.RunPass(cfg.Blocks, config);
Rewriter.RunPass(cfg.Blocks, config);
Rewriter.RunPass(hfm, cfg.Blocks, config);
}
funcs[i] = new Function(cfg.Blocks, $"fun{i}", false, inArgumentsCount, outArgumentsCount);

View File

@ -155,6 +155,9 @@ namespace Ryujinx.Graphics.Shader.Translation
{
other._config.MergeOutputUserAttributes(_config.UsedOutputAttributes, Enumerable.Empty<int>());
// We need to share the resource manager since both shaders accesses the same constant buffers.
other._config.ResourceManager = _config.ResourceManager;
FunctionCode[] otherCode = EmitShader(other._program, other._config, initializeOutputs: true, out int aStart);
code = Combine(otherCode, code, aStart);

View File

@ -65,6 +65,13 @@ namespace Ryujinx.Graphics.Vulkan
return (formatFeatureFlags & flags) == flags;
}
public bool BufferFormatSupports(FormatFeatureFlags flags, VkFormat format)
{
_api.GetPhysicalDeviceFormatProperties(_physicalDevice, format, out var fp);
return (fp.BufferFeatures & flags) == flags;
}
public bool OptimalFormatSupports(FormatFeatureFlags flags, GAL.Format format)
{
var formatFeatureFlags = _optimalTable[(int)format];

View File

@ -168,5 +168,223 @@ namespace Ryujinx.Graphics.Vulkan
{
return _table[(int)format];
}
public static int GetAttributeFormatSize(VkFormat format)
{
switch (format)
{
case VkFormat.R8Unorm:
case VkFormat.R8SNorm:
case VkFormat.R8Uint:
case VkFormat.R8Sint:
case VkFormat.R8Uscaled:
case VkFormat.R8Sscaled:
return 1;
case VkFormat.R8G8Unorm:
case VkFormat.R8G8SNorm:
case VkFormat.R8G8Uint:
case VkFormat.R8G8Sint:
case VkFormat.R8G8Uscaled:
case VkFormat.R8G8Sscaled:
case VkFormat.R16Sfloat:
case VkFormat.R16Unorm:
case VkFormat.R16SNorm:
case VkFormat.R16Uint:
case VkFormat.R16Sint:
case VkFormat.R16Uscaled:
case VkFormat.R16Sscaled:
return 2;
case VkFormat.R8G8B8Unorm:
case VkFormat.R8G8B8SNorm:
case VkFormat.R8G8B8Uint:
case VkFormat.R8G8B8Sint:
case VkFormat.R8G8B8Uscaled:
case VkFormat.R8G8B8Sscaled:
return 3;
case VkFormat.R8G8B8A8Unorm:
case VkFormat.R8G8B8A8SNorm:
case VkFormat.R8G8B8A8Uint:
case VkFormat.R8G8B8A8Sint:
case VkFormat.R8G8B8A8Srgb:
case VkFormat.R8G8B8A8Uscaled:
case VkFormat.R8G8B8A8Sscaled:
case VkFormat.B8G8R8A8Unorm:
case VkFormat.B8G8R8A8Srgb:
case VkFormat.R16G16Sfloat:
case VkFormat.R16G16Unorm:
case VkFormat.R16G16SNorm:
case VkFormat.R16G16Uint:
case VkFormat.R16G16Sint:
case VkFormat.R16G16Uscaled:
case VkFormat.R16G16Sscaled:
case VkFormat.R32Sfloat:
case VkFormat.R32Uint:
case VkFormat.R32Sint:
case VkFormat.A2B10G10R10UnormPack32:
case VkFormat.A2B10G10R10UintPack32:
case VkFormat.B10G11R11UfloatPack32:
case VkFormat.E5B9G9R9UfloatPack32:
case VkFormat.A2B10G10R10SNormPack32:
case VkFormat.A2B10G10R10SintPack32:
case VkFormat.A2B10G10R10UscaledPack32:
case VkFormat.A2B10G10R10SscaledPack32:
return 4;
case VkFormat.R16G16B16Sfloat:
case VkFormat.R16G16B16Unorm:
case VkFormat.R16G16B16SNorm:
case VkFormat.R16G16B16Uint:
case VkFormat.R16G16B16Sint:
case VkFormat.R16G16B16Uscaled:
case VkFormat.R16G16B16Sscaled:
return 6;
case VkFormat.R16G16B16A16Sfloat:
case VkFormat.R16G16B16A16Unorm:
case VkFormat.R16G16B16A16SNorm:
case VkFormat.R16G16B16A16Uint:
case VkFormat.R16G16B16A16Sint:
case VkFormat.R16G16B16A16Uscaled:
case VkFormat.R16G16B16A16Sscaled:
case VkFormat.R32G32Sfloat:
case VkFormat.R32G32Uint:
case VkFormat.R32G32Sint:
return 8;
case VkFormat.R32G32B32Sfloat:
case VkFormat.R32G32B32Uint:
case VkFormat.R32G32B32Sint:
return 12;
case VkFormat.R32G32B32A32Sfloat:
case VkFormat.R32G32B32A32Uint:
case VkFormat.R32G32B32A32Sint:
return 16;
}
return 1;
}
public static VkFormat DropLastComponent(VkFormat format)
{
switch (format)
{
case VkFormat.R8G8Unorm:
return VkFormat.R8Unorm;
case VkFormat.R8G8SNorm:
return VkFormat.R8SNorm;
case VkFormat.R8G8Uint:
return VkFormat.R8Uint;
case VkFormat.R8G8Sint:
return VkFormat.R8Sint;
case VkFormat.R8G8Uscaled:
return VkFormat.R8Uscaled;
case VkFormat.R8G8Sscaled:
return VkFormat.R8Sscaled;
case VkFormat.R8G8B8Unorm:
return VkFormat.R8G8Unorm;
case VkFormat.R8G8B8SNorm:
return VkFormat.R8G8SNorm;
case VkFormat.R8G8B8Uint:
return VkFormat.R8G8Uint;
case VkFormat.R8G8B8Sint:
return VkFormat.R8G8Sint;
case VkFormat.R8G8B8Uscaled:
return VkFormat.R8G8Uscaled;
case VkFormat.R8G8B8Sscaled:
return VkFormat.R8G8Sscaled;
case VkFormat.R8G8B8A8Unorm:
return VkFormat.R8G8B8Unorm;
case VkFormat.R8G8B8A8SNorm:
return VkFormat.R8G8B8SNorm;
case VkFormat.R8G8B8A8Uint:
return VkFormat.R8G8B8Uint;
case VkFormat.R8G8B8A8Sint:
return VkFormat.R8G8B8Sint;
case VkFormat.R8G8B8A8Srgb:
return VkFormat.R8G8B8Srgb;
case VkFormat.R8G8B8A8Uscaled:
return VkFormat.R8G8B8Uscaled;
case VkFormat.R8G8B8A8Sscaled:
return VkFormat.R8G8B8Sscaled;
case VkFormat.B8G8R8A8Unorm:
return VkFormat.B8G8R8Unorm;
case VkFormat.B8G8R8A8Srgb:
return VkFormat.B8G8R8Srgb;
case VkFormat.R16G16Sfloat:
return VkFormat.R16Sfloat;
case VkFormat.R16G16Unorm:
return VkFormat.R16Unorm;
case VkFormat.R16G16SNorm:
return VkFormat.R16SNorm;
case VkFormat.R16G16Uint:
return VkFormat.R16Uint;
case VkFormat.R16G16Sint:
return VkFormat.R16Sint;
case VkFormat.R16G16Uscaled:
return VkFormat.R16Uscaled;
case VkFormat.R16G16Sscaled:
return VkFormat.R16Sscaled;
case VkFormat.R16G16B16Sfloat:
return VkFormat.R16G16Sfloat;
case VkFormat.R16G16B16Unorm:
return VkFormat.R16G16Unorm;
case VkFormat.R16G16B16SNorm:
return VkFormat.R16G16SNorm;
case VkFormat.R16G16B16Uint:
return VkFormat.R16G16Uint;
case VkFormat.R16G16B16Sint:
return VkFormat.R16G16Sint;
case VkFormat.R16G16B16Uscaled:
return VkFormat.R16G16Uscaled;
case VkFormat.R16G16B16Sscaled:
return VkFormat.R16G16Sscaled;
case VkFormat.R16G16B16A16Sfloat:
return VkFormat.R16G16B16Sfloat;
case VkFormat.R16G16B16A16Unorm:
return VkFormat.R16G16B16Unorm;
case VkFormat.R16G16B16A16SNorm:
return VkFormat.R16G16B16SNorm;
case VkFormat.R16G16B16A16Uint:
return VkFormat.R16G16B16Uint;
case VkFormat.R16G16B16A16Sint:
return VkFormat.R16G16B16Sint;
case VkFormat.R16G16B16A16Uscaled:
return VkFormat.R16G16B16Uscaled;
case VkFormat.R16G16B16A16Sscaled:
return VkFormat.R16G16B16Sscaled;
case VkFormat.R32G32Sfloat:
return VkFormat.R32Sfloat;
case VkFormat.R32G32Uint:
return VkFormat.R32Uint;
case VkFormat.R32G32Sint:
return VkFormat.R32Sint;
case VkFormat.R32G32B32Sfloat:
return VkFormat.R32G32Sfloat;
case VkFormat.R32G32B32Uint:
return VkFormat.R32G32Uint;
case VkFormat.R32G32B32Sint:
return VkFormat.R32G32Sint;
case VkFormat.R32G32B32A32Sfloat:
return VkFormat.R32G32B32Sfloat;
case VkFormat.R32G32B32A32Uint:
return VkFormat.R32G32B32Uint;
case VkFormat.R32G32B32A32Sint:
return VkFormat.R32G32B32Sint;
}
return format;
}
}
}

View File

@ -21,6 +21,7 @@ namespace Ryujinx.Graphics.Vulkan
public uint[] AttachmentSamples { get; }
public VkFormat[] AttachmentFormats { get; }
public int[] AttachmentIndices { get; }
public uint AttachmentIntegerFormatMask { get; }
public int AttachmentsCount { get; }
public int MaxColorAttachmentIndex => AttachmentIndices.Length > 0 ? AttachmentIndices[AttachmentIndices.Length - 1] : -1;
@ -74,6 +75,7 @@ namespace Ryujinx.Graphics.Vulkan
int index = 0;
int bindIndex = 0;
uint attachmentIntegerFormatMask = 0;
foreach (ITexture color in colors)
{
@ -89,6 +91,11 @@ namespace Ryujinx.Graphics.Vulkan
AttachmentFormats[index] = texture.VkFormat;
AttachmentIndices[index] = bindIndex;
if (texture.Info.Format.IsInteger())
{
attachmentIntegerFormatMask |= 1u << bindIndex;
}
width = Math.Min(width, (uint)texture.Width);
height = Math.Min(height, (uint)texture.Height);
layers = Math.Min(layers, (uint)texture.Layers);
@ -102,6 +109,8 @@ namespace Ryujinx.Graphics.Vulkan
bindIndex++;
}
AttachmentIntegerFormatMask = attachmentIntegerFormatMask;
if (depthStencil is TextureView dsTexture && dsTexture.Valid)
{
_attachments[count - 1] = dsTexture.GetImageViewForAttachment();

View File

@ -28,6 +28,7 @@ namespace Ryujinx.Graphics.Vulkan
public readonly bool SupportsSubgroupSizeControl;
public readonly bool SupportsShaderInt8;
public readonly bool SupportsShaderStencilExport;
public readonly bool SupportsShaderStorageImageMultisample;
public readonly bool SupportsConditionalRendering;
public readonly bool SupportsExtendedDynamicState;
public readonly bool SupportsMultiView;
@ -42,6 +43,7 @@ namespace Ryujinx.Graphics.Vulkan
public readonly bool SupportsGeometryShader;
public readonly bool SupportsViewportArray2;
public readonly bool SupportsHostImportedMemory;
public readonly bool SupportsDepthClipControl;
public readonly uint MinSubgroupSize;
public readonly uint MaxSubgroupSize;
public readonly ShaderStageFlags RequiredSubgroupSizeStages;
@ -63,6 +65,7 @@ namespace Ryujinx.Graphics.Vulkan
bool supportsSubgroupSizeControl,
bool supportsShaderInt8,
bool supportsShaderStencilExport,
bool supportsShaderStorageImageMultisample,
bool supportsConditionalRendering,
bool supportsExtendedDynamicState,
bool supportsMultiView,
@ -77,6 +80,7 @@ namespace Ryujinx.Graphics.Vulkan
bool supportsGeometryShader,
bool supportsViewportArray2,
bool supportsHostImportedMemory,
bool supportsDepthClipControl,
uint minSubgroupSize,
uint maxSubgroupSize,
ShaderStageFlags requiredSubgroupSizeStages,
@ -97,6 +101,7 @@ namespace Ryujinx.Graphics.Vulkan
SupportsSubgroupSizeControl = supportsSubgroupSizeControl;
SupportsShaderInt8 = supportsShaderInt8;
SupportsShaderStencilExport = supportsShaderStencilExport;
SupportsShaderStorageImageMultisample = supportsShaderStorageImageMultisample;
SupportsConditionalRendering = supportsConditionalRendering;
SupportsExtendedDynamicState = supportsExtendedDynamicState;
SupportsMultiView = supportsMultiView;
@ -111,6 +116,7 @@ namespace Ryujinx.Graphics.Vulkan
SupportsGeometryShader = supportsGeometryShader;
SupportsViewportArray2 = supportsViewportArray2;
SupportsHostImportedMemory = supportsHostImportedMemory;
SupportsDepthClipControl = supportsDepthClipControl;
MinSubgroupSize = minSubgroupSize;
MaxSubgroupSize = maxSubgroupSize;
RequiredSubgroupSizeStages = requiredSubgroupSizeStages;

View File

@ -80,6 +80,7 @@ namespace Ryujinx.Graphics.Vulkan
private PipelineColorBlendAttachmentState[] _storedBlend;
private ulong _drawCountSinceBarrier;
public ulong DrawCount { get; private set; }
public bool RenderPassActive { get; private set; }
@ -133,6 +134,18 @@ namespace Ryujinx.Graphics.Vulkan
public unsafe void Barrier()
{
if (_drawCountSinceBarrier != DrawCount)
{
_drawCountSinceBarrier = DrawCount;
// Barriers apparently have no effect inside a render pass on MoltenVK.
// As a workaround, end the render pass.
if (Gd.IsMoltenVk)
{
EndRenderPass();
}
}
MemoryBarrier memoryBarrier = new MemoryBarrier()
{
SType = StructureType.MemoryBarrier,
@ -345,7 +358,7 @@ namespace Ryujinx.Graphics.Vulkan
public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
{
if (!_program.IsLinked)
if (!_program.IsLinked || vertexCount == 0)
{
return;
}
@ -409,7 +422,7 @@ namespace Ryujinx.Graphics.Vulkan
public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance)
{
if (!_program.IsLinked)
if (!_program.IsLinked || indexCount == 0)
{
return;
}
@ -551,7 +564,6 @@ namespace Ryujinx.Graphics.Vulkan
(uint)maxDrawCount,
(uint)stride);
}
}
else
{
@ -813,8 +825,12 @@ namespace Ryujinx.Graphics.Vulkan
public void SetDepthMode(DepthMode mode)
{
// Currently this is emulated on the shader, because Vulkan had no support for changing the depth mode.
// In the future, we may want to use the VK_EXT_depth_clip_control extension to change it here.
bool oldMode = _newState.DepthMode;
_newState.DepthMode = mode == DepthMode.MinusOneToOne;
if (_newState.DepthMode != oldMode)
{
SignalStateChange();
}
}
public void SetDepthTest(DepthTestDescriptor depthTest)
@ -1471,6 +1487,7 @@ namespace Ryujinx.Graphics.Vulkan
{
var dstAttachmentFormats = _newState.Internal.AttachmentFormats.AsSpan();
FramebufferParams.AttachmentFormats.CopyTo(dstAttachmentFormats);
_newState.Internal.AttachmentIntegerFormatMask = FramebufferParams.AttachmentIntegerFormatMask;
for (int i = FramebufferParams.AttachmentFormats.Length; i < dstAttachmentFormats.Length; i++)
{

View File

@ -165,6 +165,7 @@ namespace Ryujinx.Graphics.Vulkan
pipeline.DepthTestEnable = state.DepthTest.TestEnable;
pipeline.DepthWriteEnable = state.DepthTest.WriteEnable;
pipeline.DepthCompareOp = state.DepthTest.Func.Convert();
pipeline.DepthMode = state.DepthMode == DepthMode.MinusOneToOne;
pipeline.FrontFace = state.FrontFace.Convert();
@ -294,6 +295,7 @@ namespace Ryujinx.Graphics.Vulkan
int attachmentCount = 0;
int maxColorAttachmentIndex = -1;
uint attachmentIntegerFormatMask = 0;
for (int i = 0; i < Constants.MaxRenderTargets; i++)
{
@ -301,6 +303,11 @@ namespace Ryujinx.Graphics.Vulkan
{
pipeline.Internal.AttachmentFormats[attachmentCount++] = gd.FormatCapabilities.ConvertToVkFormat(state.AttachmentFormats[i]);
maxColorAttachmentIndex = i;
if (state.AttachmentFormats[i].IsInteger())
{
attachmentIntegerFormatMask |= 1u << i;
}
}
}
@ -311,6 +318,7 @@ namespace Ryujinx.Graphics.Vulkan
pipeline.ColorBlendAttachmentStateCount = (uint)(maxColorAttachmentIndex + 1);
pipeline.VertexAttributeDescriptionsCount = (uint)Math.Min(Constants.MaxVertexAttributes, state.VertexAttribCount);
pipeline.Internal.AttachmentIntegerFormatMask = attachmentIntegerFormatMask;
return pipeline;
}

View File

@ -1,5 +1,7 @@
using Silk.NET.Vulkan;
using Ryujinx.Common.Memory;
using Silk.NET.Vulkan;
using System;
using System.Numerics;
namespace Ryujinx.Graphics.Vulkan
{
@ -303,11 +305,19 @@ namespace Ryujinx.Graphics.Vulkan
set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFCF) | ((ulong)value << 4);
}
public bool DepthMode
{
get => ((Internal.Id9 >> 6) & 0x1) != 0UL;
set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFBF) | ((value ? 1UL : 0UL) << 6);
}
public NativeArray<PipelineShaderStageCreateInfo> Stages;
public NativeArray<PipelineShaderStageRequiredSubgroupSizeCreateInfoEXT> StageRequiredSubgroupSizes;
public PipelineLayout PipelineLayout;
public SpecData SpecializationData;
private Array32<VertexInputAttributeDescription> _vertexAttributeDescriptions2;
public void Initialize()
{
Stages = new NativeArray<PipelineShaderStageCreateInfo>(Constants.MaxShaderStages);
@ -328,6 +338,7 @@ namespace Ryujinx.Graphics.Vulkan
LineWidth = 1f;
SamplesCount = 1;
DepthMode = true;
}
public unsafe Auto<DisposablePipeline> CreateComputePipeline(
@ -400,7 +411,15 @@ namespace Ryujinx.Graphics.Vulkan
Pipeline pipelineHandle = default;
bool isMoltenVk = gd.IsMoltenVk;
if (isMoltenVk)
{
UpdateVertexAttributeDescriptions(gd);
}
fixed (VertexInputAttributeDescription* pVertexAttributeDescriptions = &Internal.VertexAttributeDescriptions[0])
fixed (VertexInputAttributeDescription* pVertexAttributeDescriptions2 = &_vertexAttributeDescriptions2[0])
fixed (VertexInputBindingDescription* pVertexBindingDescriptions = &Internal.VertexBindingDescriptions[0])
fixed (Viewport* pViewports = &Internal.Viewports[0])
fixed (Rect2D* pScissors = &Internal.Scissors[0])
@ -410,7 +429,7 @@ namespace Ryujinx.Graphics.Vulkan
{
SType = StructureType.PipelineVertexInputStateCreateInfo,
VertexAttributeDescriptionCount = VertexAttributeDescriptionsCount,
PVertexAttributeDescriptions = pVertexAttributeDescriptions,
PVertexAttributeDescriptions = isMoltenVk ? pVertexAttributeDescriptions2 : pVertexAttributeDescriptions,
VertexBindingDescriptionCount = VertexBindingDescriptionsCount,
PVertexBindingDescriptions = pVertexBindingDescriptions
};
@ -471,6 +490,17 @@ namespace Ryujinx.Graphics.Vulkan
PScissors = pScissors
};
if (gd.Capabilities.SupportsDepthClipControl)
{
var viewportDepthClipControlState = new PipelineViewportDepthClipControlCreateInfoEXT()
{
SType = StructureType.PipelineViewportDepthClipControlCreateInfoExt,
NegativeOneToOne = DepthMode
};
viewportState.PNext = &viewportDepthClipControlState;
}
var multisampleState = new PipelineMultisampleStateCreateInfo
{
SType = StructureType.PipelineMultisampleStateCreateInfo,
@ -513,6 +543,27 @@ namespace Ryujinx.Graphics.Vulkan
MaxDepthBounds = MaxDepthBounds
};
uint blendEnables = 0;
if (gd.IsMoltenVk && Internal.AttachmentIntegerFormatMask != 0)
{
// Blend can't be enabled for integer formats, so let's make sure it is disabled.
uint attachmentIntegerFormatMask = Internal.AttachmentIntegerFormatMask;
while (attachmentIntegerFormatMask != 0)
{
int i = BitOperations.TrailingZeroCount(attachmentIntegerFormatMask);
if (Internal.ColorBlendAttachmentState[i].BlendEnable)
{
blendEnables |= 1u << i;
}
Internal.ColorBlendAttachmentState[i].BlendEnable = false;
attachmentIntegerFormatMask &= ~(1u << i);
}
}
var colorBlendState = new PipelineColorBlendStateCreateInfo()
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
@ -590,6 +641,15 @@ namespace Ryujinx.Graphics.Vulkan
};
gd.Api.CreateGraphicsPipelines(device, cache, 1, &pipelineCreateInfo, null, &pipelineHandle).ThrowOnError();
// Restore previous blend enable values if we changed it.
while (blendEnables != 0)
{
int i = BitOperations.TrailingZeroCount(blendEnables);
Internal.ColorBlendAttachmentState[i].BlendEnable = true;
blendEnables &= ~(1u << i);
}
}
pipeline = new Auto<DisposablePipeline>(new DisposablePipeline(gd.Api, device, pipelineHandle));
@ -612,6 +672,62 @@ namespace Ryujinx.Graphics.Vulkan
}
}
private void UpdateVertexAttributeDescriptions(VulkanRenderer gd)
{
// Vertex attributes exceeding the stride are invalid.
// In metal, they cause glitches with the vertex shader fetching incorrect values.
// To work around this, we reduce the format to something that doesn't exceed the stride if possible.
// The assumption is that the exceeding components are not actually accessed on the shader.
for (int index = 0; index < VertexAttributeDescriptionsCount; index++)
{
var attribute = Internal.VertexAttributeDescriptions[index];
int vbIndex = GetVertexBufferIndex(attribute.Binding);
if (vbIndex >= 0)
{
ref var vb = ref Internal.VertexBindingDescriptions[vbIndex];
Format format = attribute.Format;
while (vb.Stride != 0 && attribute.Offset + FormatTable.GetAttributeFormatSize(format) > vb.Stride)
{
Format newFormat = FormatTable.DropLastComponent(format);
if (newFormat == format)
{
// That case means we failed to find a format that fits within the stride,
// so just restore the original format and give up.
format = attribute.Format;
break;
}
format = newFormat;
}
if (attribute.Format != format && gd.FormatCapabilities.BufferFormatSupports(FormatFeatureFlags.VertexBufferBit, format))
{
attribute.Format = format;
}
}
_vertexAttributeDescriptions2[index] = attribute;
}
}
private int GetVertexBufferIndex(uint binding)
{
for (int index = 0; index < VertexBindingDescriptionsCount; index++)
{
if (Internal.VertexBindingDescriptions[index].Binding == binding)
{
return index;
}
}
return -1;
}
public void Dispose()
{
Stages.Dispose();

View File

@ -35,6 +35,7 @@ namespace Ryujinx.Graphics.Vulkan
public Array16<Rect2D> Scissors;
public Array8<PipelineColorBlendAttachmentState> ColorBlendAttachmentState;
public Array9<Format> AttachmentFormats;
public uint AttachmentIntegerFormatMask;
public override bool Equals(object obj)
{

View File

@ -80,7 +80,7 @@ namespace Ryujinx.Graphics.Vulkan
var sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples);
var usage = GetImageUsageFromFormat(info.Format);
var usage = GetImageUsage(info.Format, info.Target, gd.Capabilities.SupportsShaderStorageImageMultisample);
var flags = ImageCreateFlags.CreateMutableFormatBit;
@ -293,7 +293,7 @@ namespace Ryujinx.Graphics.Vulkan
}
}
public static ImageUsageFlags GetImageUsageFromFormat(GAL.Format format)
public static ImageUsageFlags GetImageUsage(GAL.Format format, Target target, bool supportsMsStorage)
{
var usage = DefaultUsageFlags;
@ -306,7 +306,7 @@ namespace Ryujinx.Graphics.Vulkan
usage |= ImageUsageFlags.ColorAttachmentBit;
}
if (format.IsImageCompatible())
if (format.IsImageCompatible() && (supportsMsStorage || !target.IsMultisample()))
{
usage |= ImageUsageFlags.StorageBit;
}

View File

@ -54,7 +54,7 @@ namespace Ryujinx.Graphics.Vulkan
gd.Textures.Add(this);
var format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format);
var usage = TextureStorage.GetImageUsageFromFormat(info.Format);
var usage = TextureStorage.GetImageUsage(info.Format, info.Target, gd.Capabilities.SupportsShaderStorageImageMultisample);
var levels = (uint)info.Levels;
var layers = (uint)info.GetLayers();

View File

@ -41,6 +41,7 @@ namespace Ryujinx.Graphics.Vulkan
"VK_EXT_subgroup_size_control",
"VK_NV_geometry_shader_passthrough",
"VK_NV_viewport_array2",
"VK_EXT_depth_clip_control",
"VK_KHR_portability_subset" // As per spec, we should enable this if present.
};
@ -345,6 +346,17 @@ namespace Ryujinx.Graphics.Vulkan
features2.PNext = &supportedFeaturesRobustness2;
}
PhysicalDeviceDepthClipControlFeaturesEXT supportedFeaturesDepthClipControl = new PhysicalDeviceDepthClipControlFeaturesEXT()
{
SType = StructureType.PhysicalDeviceDepthClipControlFeaturesExt,
PNext = features2.PNext
};
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_depth_clip_control"))
{
features2.PNext = &supportedFeaturesDepthClipControl;
}
api.GetPhysicalDeviceFeatures2(physicalDevice.PhysicalDevice, &features2);
var supportedFeatures = features2.Features;
@ -507,6 +519,21 @@ namespace Ryujinx.Graphics.Vulkan
pExtendedFeatures = &featuresCustomBorderColor;
}
PhysicalDeviceDepthClipControlFeaturesEXT featuresDepthClipControl;
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_depth_clip_control") &&
supportedFeaturesDepthClipControl.DepthClipControl)
{
featuresDepthClipControl = new PhysicalDeviceDepthClipControlFeaturesEXT()
{
SType = StructureType.PhysicalDeviceDepthClipControlFeaturesExt,
PNext = pExtendedFeatures,
DepthClipControl = true
};
pExtendedFeatures = &featuresDepthClipControl;
}
var enabledExtensions = _requiredExtensions.Union(_desirableExtensions.Intersect(physicalDevice.DeviceExtensions)).ToArray();
IntPtr* ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Length];

View File

@ -216,6 +216,11 @@ namespace Ryujinx.Graphics.Vulkan
SType = StructureType.PhysicalDeviceCustomBorderColorFeaturesExt
};
PhysicalDeviceDepthClipControlFeaturesEXT featuresDepthClipControl = new PhysicalDeviceDepthClipControlFeaturesEXT()
{
SType = StructureType.PhysicalDeviceDepthClipControlFeaturesExt
};
PhysicalDevicePortabilitySubsetFeaturesKHR featuresPortabilitySubset = new PhysicalDevicePortabilitySubsetFeaturesKHR()
{
SType = StructureType.PhysicalDevicePortabilitySubsetFeaturesKhr
@ -244,6 +249,14 @@ namespace Ryujinx.Graphics.Vulkan
features2.PNext = &featuresCustomBorderColor;
}
bool supportsDepthClipControl = _physicalDevice.IsDeviceExtensionPresent("VK_EXT_depth_clip_control");
if (supportsDepthClipControl)
{
featuresDepthClipControl.PNext = features2.PNext;
features2.PNext = &featuresDepthClipControl;
}
bool usePortability = _physicalDevice.IsDeviceExtensionPresent("VK_KHR_portability_subset");
if (usePortability)
@ -295,6 +308,7 @@ namespace Ryujinx.Graphics.Vulkan
supportsSubgroupSizeControl,
featuresShaderInt8.ShaderInt8,
_physicalDevice.IsDeviceExtensionPresent("VK_EXT_shader_stencil_export"),
features2.Features.ShaderStorageImageMultisample,
_physicalDevice.IsDeviceExtensionPresent(ExtConditionalRendering.ExtensionName),
_physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName),
features2.Features.MultiViewport,
@ -309,6 +323,7 @@ namespace Ryujinx.Graphics.Vulkan
_physicalDevice.PhysicalDeviceFeatures.GeometryShader,
_physicalDevice.IsDeviceExtensionPresent("VK_NV_viewport_array2"),
_physicalDevice.IsDeviceExtensionPresent(ExtExternalMemoryHost.ExtensionName),
supportsDepthClipControl && featuresDepthClipControl.DepthClipControl,
propertiesSubgroupSizeControl.MinSubgroupSize,
propertiesSubgroupSizeControl.MaxSubgroupSize,
propertiesSubgroupSizeControl.RequiredSubgroupSizeStages,
@ -584,6 +599,7 @@ namespace Ryujinx.Graphics.Vulkan
supportsViewportMask: Capabilities.SupportsViewportArray2,
supportsViewportSwizzle: false,
supportsIndirectParameters: true,
supportsDepthClipControl: Capabilities.SupportsDepthClipControl,
maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage,
maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage,
maximumTexturesPerStage: Constants.MaxTexturesPerStage,
@ -599,6 +615,25 @@ namespace Ryujinx.Graphics.Vulkan
return new HardwareInfo(GpuVendor, GpuRenderer);
}
/// <summary>
/// Gets the available Vulkan devices using the default Vulkan API
/// object returned by <see cref="Vk.GetApi()"/>
/// </summary>
/// <returns></returns>
public static DeviceInfo[] GetPhysicalDevices()
{
try
{
return VulkanInitialization.GetSuitablePhysicalDevices(Vk.GetApi());
}
catch (Exception ex)
{
Logger.Error?.PrintMsg(LogClass.Gpu, $"Error querying Vulkan devices: {ex.Message}");
return Array.Empty<DeviceInfo>();
}
}
public static DeviceInfo[] GetPhysicalDevices(Vk api)
{
try

View File

@ -167,12 +167,12 @@ namespace Ryujinx.HLE.HOS
if (StrEquals(RomfsDir, modDir.Name))
{
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} RomFs>", modDir));
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(dir.Name, modDir));
types.Append('R');
}
else if (StrEquals(ExefsDir, modDir.Name))
{
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} ExeFs>", modDir));
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(dir.Name, modDir));
types.Append('E');
}
else if (StrEquals(CheatDir, modDir.Name))

View File

@ -1,4 +1,5 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel;
@ -32,13 +33,14 @@ namespace Ryujinx.HLE.HOS.Services
0x01007FFF
};
private readonly object _handleLock = new();
// The amount of time Dispose() will wait to Join() the thread executing the ServerLoop()
private static readonly TimeSpan ThreadJoinTimeout = TimeSpan.FromSeconds(3);
private readonly KernelContext _context;
private KProcess _selfProcess;
private KThread _selfThread;
private readonly List<int> _sessionHandles = new List<int>();
private readonly List<int> _portHandles = new List<int>();
private readonly ReaderWriterLockSlim _handleLock = new ReaderWriterLockSlim();
private readonly Dictionary<int, IpcService> _sessions = new Dictionary<int, IpcService>();
private readonly Dictionary<int, Func<IpcService>> _ports = new Dictionary<int, Func<IpcService>>();
@ -48,6 +50,8 @@ namespace Ryujinx.HLE.HOS.Services
private readonly MemoryStream _responseDataStream;
private readonly BinaryWriter _responseDataWriter;
private int _isDisposed = 0;
public ManualResetEvent InitDone { get; }
public string Name { get; }
public Func<IpcService> SmObjectFactory { get; }
@ -79,11 +83,20 @@ namespace Ryujinx.HLE.HOS.Services
private void AddPort(int serverPortHandle, Func<IpcService> objectFactory)
{
lock (_handleLock)
bool lockTaken = false;
try
{
_portHandles.Add(serverPortHandle);
lockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
_ports.Add(serverPortHandle, objectFactory);
}
finally
{
if (lockTaken)
{
_handleLock.ExitWriteLock();
}
}
_ports.Add(serverPortHandle, objectFactory);
}
public void AddSessionObj(KServerSession serverSession, IpcService obj)
@ -92,16 +105,62 @@ namespace Ryujinx.HLE.HOS.Services
InitDone.WaitOne();
_selfProcess.HandleTable.GenerateHandle(serverSession, out int serverSessionHandle);
AddSessionObj(serverSessionHandle, obj);
}
public void AddSessionObj(int serverSessionHandle, IpcService obj)
{
lock (_handleLock)
bool lockTaken = false;
try
{
_sessionHandles.Add(serverSessionHandle);
lockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
_sessions.Add(serverSessionHandle, obj);
}
finally
{
if (lockTaken)
{
_handleLock.ExitWriteLock();
}
}
}
private IpcService GetSessionObj(int serverSessionHandle)
{
bool lockTaken = false;
try
{
lockTaken = _handleLock.TryEnterReadLock(Timeout.Infinite);
return _sessions[serverSessionHandle];
}
finally
{
if (lockTaken)
{
_handleLock.ExitReadLock();
}
}
}
private bool RemoveSessionObj(int serverSessionHandle, out IpcService obj)
{
bool lockTaken = false;
try
{
lockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
return _sessions.Remove(serverSessionHandle, out obj);
}
finally
{
if (lockTaken)
{
_handleLock.ExitWriteLock();
}
}
_sessions.Add(serverSessionHandle, obj);
}
private void Main()
@ -112,6 +171,7 @@ namespace Ryujinx.HLE.HOS.Services
private void ServerLoop()
{
_selfProcess = KernelStatic.GetCurrentProcess();
_selfThread = KernelStatic.GetCurrentThread();
if (SmObjectFactory != null)
{
@ -122,8 +182,7 @@ namespace Ryujinx.HLE.HOS.Services
InitDone.Set();
KThread thread = KernelStatic.GetCurrentThread();
ulong messagePtr = thread.TlsAddress;
ulong messagePtr = _selfThread.TlsAddress;
_context.Syscall.SetHeapSize(out ulong heapAddr, 0x200000);
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
@ -134,27 +193,39 @@ namespace Ryujinx.HLE.HOS.Services
while (true)
{
int handleCount;
int portHandleCount;
int handleCount;
int[] handles;
lock (_handleLock)
bool handleLockTaken = false;
try
{
portHandleCount = _portHandles.Count;
handleCount = portHandleCount + _sessionHandles.Count;
handleLockTaken = _handleLock.TryEnterReadLock(Timeout.Infinite);
portHandleCount = _ports.Count;
handleCount = portHandleCount + _sessions.Count;
handles = ArrayPool<int>.Shared.Rent(handleCount);
_portHandles.CopyTo(handles, 0);
_sessionHandles.CopyTo(handles, portHandleCount);
_ports.Keys.CopyTo(handles, 0);
_sessions.Keys.CopyTo(handles, portHandleCount);
}
finally
{
if (handleLockTaken)
{
_handleLock.ExitReadLock();
}
}
// We still need a timeout here to allow the service to pick up and listen new sessions...
var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, 1000000L);
thread.HandlePostSyscall();
_selfThread.HandlePostSyscall();
if (!thread.Context.Running)
if (!_selfThread.Context.Running)
{
break;
}
@ -178,9 +249,20 @@ namespace Ryujinx.HLE.HOS.Services
// We got a new connection, accept the session to allow servicing future requests.
if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
{
IpcService obj = _ports[handles[signaledIndex]].Invoke();
AddSessionObj(serverSessionHandle, obj);
bool handleWriteLockTaken = false;
try
{
handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
IpcService obj = _ports[handles[signaledIndex]].Invoke();
_sessions.Add(serverSessionHandle, obj);
}
finally
{
if (handleWriteLockTaken)
{
_handleLock.ExitWriteLock();
}
}
}
}
@ -197,11 +279,7 @@ namespace Ryujinx.HLE.HOS.Services
private bool Process(int serverSessionHandle, ulong recvListAddr)
{
KProcess process = KernelStatic.GetCurrentProcess();
KThread thread = KernelStatic.GetCurrentThread();
ulong messagePtr = thread.TlsAddress;
IpcMessage request = ReadRequest(process, messagePtr);
IpcMessage request = ReadRequest();
IpcMessage response = new IpcMessage();
@ -247,15 +325,15 @@ namespace Ryujinx.HLE.HOS.Services
ServiceCtx context = new ServiceCtx(
_context.Device,
process,
process.CpuMemory,
thread,
_selfProcess,
_selfProcess.CpuMemory,
_selfThread,
request,
response,
_requestDataReader,
_responseDataWriter);
_sessions[serverSessionHandle].CallCmifMethod(context);
GetSessionObj(serverSessionHandle).CallCmifMethod(context);
response.RawData = _responseDataStream.ToArray();
}
@ -268,7 +346,7 @@ namespace Ryujinx.HLE.HOS.Services
switch (cmdId)
{
case 0:
FillHipcResponse(response, 0, _sessions[serverSessionHandle].ConvertToDomain());
FillHipcResponse(response, 0, GetSessionObj(serverSessionHandle).ConvertToDomain());
break;
case 3:
@ -278,17 +356,31 @@ namespace Ryujinx.HLE.HOS.Services
// TODO: Whats the difference between IpcDuplicateSession/Ex?
case 2:
case 4:
int unknown = _requestDataReader.ReadInt32();
{
_ = _requestDataReader.ReadInt32();
_context.Syscall.CreateSession(out int dupServerSessionHandle, out int dupClientSessionHandle, false, 0);
_context.Syscall.CreateSession(out int dupServerSessionHandle, out int dupClientSessionHandle, false, 0);
AddSessionObj(dupServerSessionHandle, _sessions[serverSessionHandle]);
bool writeLockTaken = false;
try
{
writeLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
_sessions[dupServerSessionHandle] = _sessions[serverSessionHandle];
}
finally
{
if (writeLockTaken)
{
_handleLock.ExitWriteLock();
}
}
response.HandleDesc = IpcHandleDesc.MakeMove(dupClientSessionHandle);
response.HandleDesc = IpcHandleDesc.MakeMove(dupClientSessionHandle);
FillHipcResponse(response, 0);
FillHipcResponse(response, 0);
break;
break;
}
default: throw new NotImplementedException(cmdId.ToString());
}
@ -296,13 +388,10 @@ namespace Ryujinx.HLE.HOS.Services
else if (request.Type == IpcMessageType.CmifCloseSession || request.Type == IpcMessageType.TipcCloseSession)
{
_context.Syscall.CloseHandle(serverSessionHandle);
lock (_handleLock)
if (RemoveSessionObj(serverSessionHandle, out var session))
{
_sessionHandles.Remove(serverSessionHandle);
(session as IDisposable)?.Dispose();
}
IpcService service = _sessions[serverSessionHandle];
(service as IDisposable)?.Dispose();
_sessions.Remove(serverSessionHandle);
shouldReply = false;
}
// If the type is past 0xF, we are using TIPC
@ -317,20 +406,20 @@ namespace Ryujinx.HLE.HOS.Services
ServiceCtx context = new ServiceCtx(
_context.Device,
process,
process.CpuMemory,
thread,
_selfProcess,
_selfProcess.CpuMemory,
_selfThread,
request,
response,
_requestDataReader,
_responseDataWriter);
_sessions[serverSessionHandle].CallTipcMethod(context);
GetSessionObj(serverSessionHandle).CallTipcMethod(context);
response.RawData = _responseDataStream.ToArray();
using var responseStream = response.GetStreamTipc();
process.CpuMemory.Write(messagePtr, responseStream.GetReadOnlySequence());
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
}
else
{
@ -339,27 +428,24 @@ namespace Ryujinx.HLE.HOS.Services
if (!isTipcCommunication)
{
using var responseStream = response.GetStream((long)messagePtr, recvListAddr | ((ulong)PointerBufferSize << 48));
process.CpuMemory.Write(messagePtr, responseStream.GetReadOnlySequence());
using var responseStream = response.GetStream((long)_selfThread.TlsAddress, recvListAddr | ((ulong)PointerBufferSize << 48));
_selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
}
return shouldReply;
}
private static IpcMessage ReadRequest(KProcess process, ulong messagePtr)
private IpcMessage ReadRequest()
{
const int messageSize = 0x100;
byte[] reqData = ArrayPool<byte>.Shared.Rent(messageSize);
using IMemoryOwner<byte> reqDataOwner = ByteMemoryPool.Shared.Rent(messageSize);
Span<byte> reqDataSpan = reqData.AsSpan(0, messageSize);
reqDataSpan.Clear();
Span<byte> reqDataSpan = reqDataOwner.Memory.Span;
process.CpuMemory.Read(messagePtr, reqDataSpan);
_selfProcess.CpuMemory.Read(_selfThread.TlsAddress, reqDataSpan);
IpcMessage request = new IpcMessage(reqDataSpan, (long)messagePtr);
ArrayPool<byte>.Shared.Return(reqData);
IpcMessage request = new IpcMessage(reqDataSpan, (long)_selfThread.TlsAddress);
return request;
}
@ -392,26 +478,35 @@ namespace Ryujinx.HLE.HOS.Services
protected virtual void Dispose(bool disposing)
{
if (disposing)
if (disposing && _selfThread != null)
{
foreach (IpcService service in _sessions.Values)
if (_selfThread.HostThread.ManagedThreadId != Environment.CurrentManagedThreadId && _selfThread.HostThread.Join(ThreadJoinTimeout) == false)
{
if (service is IDisposable disposableObj)
{
disposableObj.Dispose();
}
Logger.Warning?.Print(LogClass.Service, $"The ServerBase thread didn't terminate within {ThreadJoinTimeout:g}, waiting longer.");
service.DestroyAtExit();
_selfThread.HostThread.Join(Timeout.Infinite);
}
_sessions.Clear();
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
{
foreach (IpcService service in _sessions.Values)
{
(service as IDisposable)?.Dispose();
_requestDataReader.Dispose();
_requestDataStream.Dispose();
_responseDataWriter.Dispose();
_responseDataStream.Dispose();
service.DestroyAtExit();
}
InitDone.Dispose();
_sessions.Clear();
_ports.Clear();
_handleLock.Dispose();
_requestDataReader.Dispose();
_requestDataStream.Dispose();
_responseDataWriter.Dispose();
_responseDataStream.Dispose();
InitDone.Dispose();
}
}
}

View File

@ -130,7 +130,7 @@ namespace Ryujinx.Headless.SDL2
public float AudioVolume { get; set; }
[Option("use-hypervisor", Required = false, Default = true, HelpText = "Uses Hypervisor over JIT if available.")]
public bool UseHypervisor { get; set; }
public bool? UseHypervisor { get; set; }
[Option("lan-interface-id", Required = false, Default = "0", HelpText = "GUID for the network interface used by LAN.")]
public string MultiplayerLanInterfaceId { get; set; }

View File

@ -339,6 +339,15 @@ namespace Ryujinx.Headless.SDL2
GraphicsConfig.EnableShaderCache = true;
if (OperatingSystem.IsMacOS())
{
if (option.GraphicsBackend == GraphicsBackend.OpenGl)
{
option.GraphicsBackend = GraphicsBackend.Vulkan;
Logger.Warning?.Print(LogClass.Application, "OpenGL is not supported on macOS, switching to Vulkan!");
}
}
IGamepad gamepad;
if (option.ListInputIds)
@ -550,7 +559,7 @@ namespace Ryujinx.Headless.SDL2
options.IgnoreMissingServices,
options.AspectRatio,
options.AudioVolume,
options.UseHypervisor,
options.UseHypervisor ?? true,
options.MultiplayerLanInterfaceId);
return new Switch(configuration);
@ -703,4 +712,4 @@ namespace Ryujinx.Headless.SDL2
return true;
}
}
}
}

View File

@ -7,6 +7,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<SigningCertificate Condition=" '$(SigningCertificate)' == '' ">-</SigningCertificate>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
@ -15,6 +16,10 @@
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
<Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
</Target>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
@ -29,6 +34,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win10-x64'" />
</ItemGroup>
<ItemGroup>
@ -63,4 +69,4 @@
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>
</Project>
</Project>

View File

@ -62,6 +62,7 @@ namespace Ryujinx.Headless.SDL2
private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
private readonly ManualResetEvent _exitEvent;
private readonly ManualResetEvent _gpuDoneEvent;
private long _ticks;
private bool _isActive;
@ -91,6 +92,7 @@ namespace Ryujinx.Headless.SDL2
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
HostUiTheme = new HeadlessHostUiTheme();
@ -275,6 +277,14 @@ namespace Ryujinx.Headless.SDL2
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
}
}
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
});
FinalizeWindowRenderer();
@ -404,7 +414,10 @@ namespace Ryujinx.Headless.SDL2
MainLoop();
renderLoopThread.Join();
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
nvStutterWorkaround?.Join();
Exit();

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
@ -53,9 +54,9 @@ namespace Ryujinx.Memory
IntPtr ptr = mmap(IntPtr.Zero, size, prot, flags, -1, 0);
if (ptr == new IntPtr(-1L))
if (ptr == MAP_FAILED)
{
throw new OutOfMemoryException();
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
if (!_allocations.TryAdd(ptr, size))
@ -76,17 +77,33 @@ namespace Ryujinx.Memory
prot |= MmapProts.PROT_EXEC;
}
return mprotect(address, size, prot) == 0;
if (mprotect(address, size, prot) != 0)
{
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
return true;
}
public static bool Decommit(IntPtr address, ulong size)
{
// Must be writable for madvise to work properly.
mprotect(address, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE);
if (mprotect(address, size, MmapProts.PROT_READ | MmapProts.PROT_WRITE) != 0)
{
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
madvise(address, size, MADV_REMOVE);
if (madvise(address, size, MADV_REMOVE) != 0)
{
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
return mprotect(address, size, MmapProts.PROT_NONE) == 0;
if (mprotect(address, size, MmapProts.PROT_NONE) != 0)
{
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
return true;
}
public static bool Reprotect(IntPtr address, ulong size, MemoryPermission permission)

View File

@ -1,5 +1,6 @@
using Ryujinx.Memory.WindowsShared;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Memory
@ -36,7 +37,7 @@ namespace Ryujinx.Memory
if (ptr == IntPtr.Zero)
{
throw new OutOfMemoryException();
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
return ptr;
@ -48,7 +49,7 @@ namespace Ryujinx.Memory
if (ptr == IntPtr.Zero)
{
throw new OutOfMemoryException();
throw new SystemException(Marshal.GetLastPInvokeErrorMessage());
}
return ptr;

View File

@ -1,8 +1,11 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Memory
{
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static partial class MemoryManagerUnixHelper
{
[Flags]
@ -41,6 +44,8 @@ namespace Ryujinx.Memory
O_SYNC = 256,
}
public const IntPtr MAP_FAILED = -1;
private const int MAP_ANONYMOUS_LINUX_GENERIC = 0x20;
private const int MAP_NORESERVE_LINUX_GENERIC = 0x4000;
private const int MAP_UNLOCKED_LINUX_GENERIC = 0x80000;

View File

@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Memory
{
@ -8,7 +9,7 @@ namespace Ryujinx.Memory
{
}
public MemoryProtectionException(MemoryPermission permission) : base($"Failed to set memory protection to \"{permission}\".")
public MemoryProtectionException(MemoryPermission permission) : base($"Failed to set memory protection to \"{permission}\": {Marshal.GetLastPInvokeErrorMessage()}")
{
}

View File

@ -1,8 +1,10 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Memory.WindowsShared
{
[SupportedOSPlatform("windows")]
static partial class WindowsApi
{
public static readonly IntPtr InvalidHandleValue = new IntPtr(-1);

View File

@ -1,7 +1,9 @@
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Memory.WindowsShared
{
[SupportedOSPlatform("windows")]
class WindowsApiException : Exception
{
public WindowsApiException()

View File

@ -0,0 +1,62 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Versioning;
namespace Ryujinx.Ui.Common.Helper
{
[SupportedOSPlatform("linux")]
public static class LinuxHelper
{
// NOTE: This value was determined by manual tests and might need to be increased again.
public const int RecommendedVmMaxMapCount = 524288;
public const string VmMaxMapCountPath = "/proc/sys/vm/max_map_count";
public const string SysCtlConfigPath = "/etc/sysctl.d/99-Ryujinx.conf";
public static int VmMaxMapCount => int.Parse(File.ReadAllText(VmMaxMapCountPath));
public static string PkExecPath { get; } = GetBinaryPath("pkexec");
private static string GetBinaryPath(string binary)
{
string pathVar = Environment.GetEnvironmentVariable("PATH");
if (pathVar is null || string.IsNullOrEmpty(binary))
{
return null;
}
foreach (var searchPath in pathVar.Split(":", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
string binaryPath = Path.Combine(searchPath, binary);
if (File.Exists(binaryPath))
{
return binaryPath;
}
}
return null;
}
public static int RunPkExec(string command)
{
if (PkExecPath == null)
{
return 1;
}
using Process process = new()
{
StartInfo =
{
FileName = PkExecPath,
ArgumentList = { "sh", "-c", command }
}
};
process.Start();
process.WaitForExit();
return process.ExitCode;
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

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