Compare commits

...

15 Commits

Author SHA1 Message Date
ashuk
e5c9838b0b Correct tooltips for add,remove,removeall buttons (#4819) 2023-05-05 22:38:57 +02:00
Ikko Eltociear Ashimine
f8ec878796 Fix typo in TextureBindingsManager.cs (#4798)
accomodate -> accommodate
2023-05-05 22:17:36 +02:00
Skyth (Asilkan)
9ff21f9ab6 Use ToLowerInvariant when detecting GPU vendor. (#4815) 2023-05-05 16:35:59 +00:00
gdkchan
aa021085cf Allow any shader SSBO constant buffer slot and offset (#2237)
* Allow any shader SSBO constant buffer slot and offset

* Fix slot value passed to SetUsedStorageBuffer on fallback case

* Shader cache version

* Ensure that the storage buffer source constant buffer offset is word aligned

* Fix FirstBinding on GetUniformBufferDescriptors
2023-05-05 14:20:20 +00:00
riperiperi
1f5d881860 GPU: Allow granular buffer updates from the constant buffer updater (#4749)
* GPU: Allow granular buffer updates from the constant buffer updater

Sometimes, constant buffer updates can't be avoided, either due to a cb0 access that cannot be eliminated, or the game updating a buffer between draws to the detriment of everyone.

To avoid uploading the full 4096 bytes each time, this PR remembers the offset and size containing all constant buffer updates since the last sync. It will then upload that range after sync.

* Allow clearing the dirty range

* Always use precise

Might want to not do this if distance between the existing range and new one is too high.

* Use old force dirty mechanism when distance between regions is too great

* Update src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs

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

* Fix inheritance of _dirtyStart and _dirtyEnd

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2023-05-05 13:47:15 +00:00
TSRBerry
1f664100bd ModLoader: Fix case sensitivy issues without breaking cheats (#4783)
* Fix case sensitivity for mod subdirectories

* Small refactoring of ModLoader

* Don't share instruction list between all cheats

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

---------

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
2023-05-05 09:39:08 +02:00
Horizon
1f5e1ffa80 fix: linux launcher breaks when there are spaces in the directory path (#4795)
* fix: linux launcher breaks when there are spaces in the directory path

* Add quotes around $0 as well

---------

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
2023-05-05 09:06:15 +02:00
gdkchan
264438ff19 Revert "bcat ipc (#4446)" (#4801)
This reverts commit 4250732353.
2023-05-04 18:16:51 +02:00
Ac_K
3b8ac1641a UI: Move ApplicationContextMenu in a separated class (#4755)
* UI: Move ApplicationContextMenu in a separated class

This PR remove duplicated code related to the context menu on the Application list/grid by create a control for the menu which include related handler.

I've renamed "GameList/GameGrid" by "Application" for consistencies. And I've removed all uneeded field from the project file too.

While I cleaned up things, I've found an issue about purging Ptc/Shader cache, both methods list files even if the user say "No", shader cache is purged even if the user say "No". It's fixed.

* Adresses feedbacks
2023-05-04 14:41:06 +00:00
Emmanuel Hansen
4250732353 bcat ipc (#4446) 2023-05-04 16:26:10 +02:00
gdkchan
4d1579acbf Fix some invalid blits involving depth textures (#4723) 2023-05-03 21:20:12 -03:00
Erdem Keskin
6279f5e430 Update SettingsWindow.cs (#4785)
fix saving if directory path directly pasted in to the text field instead of using FileChooser.
2023-05-03 16:04:40 +02:00
Mary
b7d2bff6aa Revert "ModLoader: Fix case sensitivy issues (#4720)" (#4781)
This reverts commit cc1a933a2f.
2023-05-03 11:20:05 +02:00
riperiperi
7c327fecb3 Vulkan: Record modifications after changing the framebuffer (#4775)
Our Vulkan backend inserts image barriers when a texture is sampled after it is rendered. This is done via a "modification flag" which is set when a render target is unbound (presuming that a texture has finished drawing to it).

Imagine the following scenario:
- Game sets render target to texture A
- Game renders to texture A
- (render pass ends)
- Game binds texture A to a sampler
- Game sets render target to texture B
- Renders to texture B using texture A (barrier required)

Because of the previous behaviour, the check to add a barrier for sampling a texture actually happens before it is registered as modified, meaning no barrier was added at all. This isn't always the case, but it was definitely causing issues in Xenoblade 2.

This doesn't fix any more complicated issues where a texture is repeatedly sampled while it is currently being rendered.

Fixes visual glitches at lower resolutions in Xenoblade 2. May fix other cases.
2023-05-03 10:42:21 +02:00
TSRBerry
cc1a933a2f ModLoader: Fix case sensitivy issues (#4720)
* Fix case sensitivity for mod subdirectories

* Small refactoring of ModLoader
2023-05-03 02:07:16 +02:00
40 changed files with 1017 additions and 798 deletions

View File

@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
SCRIPT_DIR=$(dirname $(realpath $0)) SCRIPT_DIR=$(dirname "$(realpath "$0")")
RYUJINX_BIN="Ryujinx" RYUJINX_BIN="Ryujinx"
if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then

View File

@@ -103,50 +103,6 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</AvaloniaResource> </AvaloniaResource>
<AvaloniaResource Include="Assets\Styles\Styles.xaml" /> <AvaloniaResource Include="Assets\Styles\Styles.xaml" />
<Compile Update="App.axaml.cs">
<DependentUpon>App.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Ui\Windows\MainWindow.axaml.cs">
<DependentUpon>MainWindow.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Ui\Windows\AboutWindow.axaml.cs">
<DependentUpon>AboutWindow.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Ui\Applet\ErrorAppletWindow.axaml.cs">
<DependentUpon>ProfileWindow.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Ui\Applet\SwkbdAppletWindow.axaml.cs">
<DependentUpon>ProfileWindow.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Ui\Controls\InputDialog.axaml.cs">
<DependentUpon>InputDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Ui\Windows\ContentDialogOverlay.xaml.cs">
<DependentUpon>ContentDialogOverlay.xaml</DependentUpon>
</Compile>
<Compile Update="Ui\Controls\GameListView.axaml.cs">
<DependentUpon>GameListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="UI\Views\User\UserEditorView.axaml.cs">
<DependentUpon>UserEditor.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="UI\Views\User\UserRecovererView.axaml.cs">
<DependentUpon>UserRecoverer.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="UI\Views\User\UserSelectorView.axaml.cs">
<DependentUpon>UserSelector.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,80 @@
<MenuFlyout
x:Class="Ryujinx.Ava.UI.Controls.ApplicationContextMenu"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale">
<MenuItem
Click="ToggleFavorite_Click"
Header="{locale:Locale GameListContextMenuToggleFavorite}"
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
<Separator />
<MenuItem
Click="OpenUserSaveDirectory_Click"
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
IsEnabled="{Binding OpenUserSaveDirectoryEnabled}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
<MenuItem
Click="OpenDeviceSaveDirectory_Click"
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
IsEnabled="{Binding OpenDeviceSaveDirectoryEnabled}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
<MenuItem
Click="OpenBcatSaveDirectory_Click"
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
IsEnabled="{Binding OpenBcatSaveDirectoryEnabled}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
<Separator />
<MenuItem
Click="OpenTitleUpdateManager_Click"
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
<MenuItem
Click="OpenDownloadableContentManager_Click"
Header="{locale:Locale GameListContextMenuManageDlc}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
<MenuItem
Click="OpenCheatManager_Click"
Header="{locale:Locale GameListContextMenuManageCheat}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
<MenuItem
Click="OpenModsDirectory_Click"
Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
<MenuItem
Click="OpenSdModsDirectory_Click"
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
<Separator />
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
<MenuItem
Click="PurgePtcCache_Click"
Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
<MenuItem
Click="PurgeShaderCache_Click"
Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
<MenuItem
Click="OpenPtcDirectory_Click"
Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
<MenuItem
Click="OpenShaderCacheDirectory_Click"
Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
</MenuItem>
<MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
<MenuItem
Click="ExtractApplicationLogo_Click"
Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
<MenuItem
Click="ExtractApplicationRomFs_Click"
Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Click="ExtractApplicationExeFs_Click"
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
</MenuItem>
</MenuFlyout>

View File

@@ -0,0 +1,322 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.HOS;
using Ryujinx.Ui.Common.Helper;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Path = System.IO.Path;
using UserId = LibHac.Fs.UserId;
namespace Ryujinx.Ava.UI.Controls
{
public class ApplicationContextMenu : MenuFlyout
{
public ApplicationContextMenu()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void ToggleFavorite_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
viewModel.ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata =>
{
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
});
viewModel.RefreshView();
}
}
public void OpenUserSaveDirectory_Click(object sender, RoutedEventArgs args)
{
if ((sender as MenuItem)?.DataContext is MainWindowViewModel viewModel)
{
OpenSaveDirectory(viewModel, SaveDataType.Account, userId: new UserId((ulong)viewModel.AccountManager.LastOpenedUser.UserId.High, (ulong)viewModel.AccountManager.LastOpenedUser.UserId.Low));
}
}
public void OpenDeviceSaveDirectory_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
OpenSaveDirectory(viewModel, SaveDataType.Device, userId: default);
}
public void OpenBcatSaveDirectory_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
OpenSaveDirectory(viewModel, SaveDataType.Bcat, userId: default);
}
private static void OpenSaveDirectory(MainWindowViewModel viewModel, SaveDataType saveDataType, UserId userId)
{
if (viewModel?.SelectedApplication != null)
{
if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
});
return;
}
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
}
}
public async void OpenTitleUpdateManager_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
}
}
public async void OpenDownloadableContentManager_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
}
}
public async void OpenCheatManager_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await new CheatWindow(viewModel.VirtualFileSystem, viewModel.SelectedApplication.TitleId, viewModel.SelectedApplication.TitleName).ShowDialog(viewModel.TopLevel as Window);
}
}
public void OpenModsDirectory_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath);
}
}
public void OpenSdModsDirectory_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
string sdModsBasePath = ModLoader.GetSdModsBasePath();
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath);
}
}
public async void PurgePtcCache_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes)
{
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0"));
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1"));
List<FileInfo> cacheFiles = new();
if (mainDir.Exists)
{
cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache"));
}
if (backupDir.Exists)
{
cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache"));
}
if (cacheFiles.Count > 0)
{
foreach (FileInfo file in cacheFiles)
{
try
{
file.Delete();
}
catch (Exception ex)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex));
}
}
}
}
}
}
public async void PurgeShaderCache_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes)
{
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"));
List<DirectoryInfo> oldCacheDirectories = new();
List<FileInfo> newCacheFiles = new();
if (shaderCacheDir.Exists)
{
oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*"));
newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc"));
newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data"));
}
if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0))
{
foreach (DirectoryInfo directory in oldCacheDirectories)
{
try
{
directory.Delete(true);
}
catch (Exception ex)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, directory.Name, ex));
}
}
foreach (FileInfo file in newCacheFiles)
{
try
{
file.Delete();
}
catch (Exception ex)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.ShaderCachePurgeError, file.Name, ex));
}
}
}
}
}
}
public void OpenPtcDirectory_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu");
string mainDir = Path.Combine(ptcDir, "0");
string backupDir = Path.Combine(ptcDir, "1");
if (!Directory.Exists(ptcDir))
{
Directory.CreateDirectory(ptcDir);
Directory.CreateDirectory(mainDir);
Directory.CreateDirectory(backupDir);
}
OpenHelper.OpenFolder(ptcDir);
}
}
public void OpenShaderCacheDirectory_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
Directory.CreateDirectory(shaderCacheDir);
}
OpenHelper.OpenFolder(shaderCacheDir);
}
}
public async void ExtractApplicationLogo_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, viewModel.SelectedApplication.Path, viewModel.SelectedApplication.TitleName);
}
}
public async void ExtractApplicationRomFs_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await ApplicationHelper.ExtractSection(NcaSectionType.Data, viewModel.SelectedApplication.Path, viewModel.SelectedApplication.TitleName);
}
}
public async void ExtractApplicationExeFs_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await ApplicationHelper.ExtractSection(NcaSectionType.Code, viewModel.SelectedApplication.Path, viewModel.SelectedApplication.TitleName);
}
}
}
}

View File

@@ -0,0 +1,102 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Controls.ApplicationGridView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DesignHeight="450"
d:DesignWidth="800"
Focusable="True"
mc:Ignorable="d">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
<controls:ApplicationContextMenu x:Key="ApplicationContextMenu" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0"
Padding="8"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ContextFlyout="{StaticResource ApplicationContextMenu}"
DoubleTapped="GameList_DoubleTapped"
Items="{Binding AppsObservableList}"
SelectionChanged="GameList_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<flex:FlexPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlignContent="FlexStart"
JustifyContent="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Border
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
ClipToBounds="True"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<Panel
Grid.Row="1"
Height="50"
Margin="0,10,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}">
<TextBlock
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding TitleName}"
TextAlignment="Center"
TextWrapping="Wrap" />
</Panel>
</Grid>
</Border>
<ui:SymbolIcon
Margin="5,5,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
FontSize="16"
Foreground="{DynamicResource SystemAccentColor}"
IsVisible="{Binding Favorite}"
Symbol="StarFilled" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@@ -9,10 +9,10 @@ using System;
namespace Ryujinx.Ava.UI.Controls namespace Ryujinx.Ava.UI.Controls
{ {
public partial class GameGridView : UserControl public partial class ApplicationGridView : UserControl
{ {
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent = public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); RoutedEvent.Register<ApplicationGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
{ {
@@ -20,7 +20,7 @@ namespace Ryujinx.Ava.UI.Controls
remove { RemoveHandler(ApplicationOpenedEvent, value); } remove { RemoveHandler(ApplicationOpenedEvent, value); }
} }
public GameGridView() public ApplicationGridView()
{ {
InitializeComponent(); InitializeComponent();
} }
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.Controls
} }
} }
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) private void SearchBox_OnKeyUp(object sender, KeyEventArgs args)
{ {
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
} }

View File

@@ -1,10 +1,10 @@
<UserControl <UserControl
x:Class="Ryujinx.Ava.UI.Controls.GameListView" x:Class="Ryujinx.Ava.UI.Controls.ApplicationListView"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DesignHeight="450" d:DesignHeight="450"
@@ -13,82 +13,7 @@
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" /> <helpers:BitmapArrayValueConverter x:Key="ByteImage" />
<MenuFlyout x:Key="GameContextMenu"> <controls:ApplicationContextMenu x:Key="ApplicationContextMenu" />
<MenuItem
Command="{Binding ToggleFavorite}"
Header="{locale:Locale GameListContextMenuToggleFavorite}"
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenUserSaveDirectory}"
IsEnabled="{Binding EnabledUserSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenDeviceSaveDirectory}"
IsEnabled="{Binding EnabledDeviceSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenBcatSaveDirectory}"
IsEnabled="{Binding EnabledBcatSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenTitleUpdateManager}"
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
<MenuItem
Command="{Binding OpenDownloadableContentManager}"
Header="{locale:Locale GameListContextMenuManageDlc}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
<MenuItem
Command="{Binding OpenCheatManager}"
Header="{locale:Locale GameListContextMenuManageCheat}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
<MenuItem
Command="{Binding OpenModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenSdModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
<Separator />
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
<MenuItem
Command="{Binding PurgePtcCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
<MenuItem
Command="{Binding PurgeShaderCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
<MenuItem
Command="{Binding OpenPtcDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenShaderCacheDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
</MenuItem>
<MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
<MenuItem
Command="{Binding ExtractExeFs}"
Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
<MenuItem
Command="{Binding ExtractRomFs}"
Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Command="{Binding ExtractLogo}"
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
</MenuItem>
</MenuFlyout>
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -100,7 +25,7 @@
Padding="8" Padding="8"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ContextFlyout="{StaticResource GameContextMenu}" ContextFlyout="{StaticResource ApplicationContextMenu}"
DoubleTapped="GameList_DoubleTapped" DoubleTapped="GameList_DoubleTapped"
Items="{Binding AppsObservableList}" Items="{Binding AppsObservableList}"
SelectionChanged="GameList_SelectionChanged"> SelectionChanged="GameList_SelectionChanged">

View File

@@ -9,10 +9,10 @@ using System;
namespace Ryujinx.Ava.UI.Controls namespace Ryujinx.Ava.UI.Controls
{ {
public partial class GameListView : UserControl public partial class ApplicationListView : UserControl
{ {
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent = public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); RoutedEvent.Register<ApplicationListView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
{ {
@@ -20,7 +20,7 @@ namespace Ryujinx.Ava.UI.Controls
remove { RemoveHandler(ApplicationOpenedEvent, value); } remove { RemoveHandler(ApplicationOpenedEvent, value); }
} }
public GameListView() public ApplicationListView()
{ {
InitializeComponent(); InitializeComponent();
} }
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.Controls
} }
} }
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) private void SearchBox_OnKeyUp(object sender, KeyEventArgs args)
{ {
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
} }

View File

@@ -1,177 +0,0 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Controls.GameGridView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
Focusable="True">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
<MenuFlyout x:Key="GameContextMenu">
<MenuItem
Command="{Binding ToggleFavorite}"
Header="{locale:Locale GameListContextMenuToggleFavorite}"
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenUserSaveDirectory}"
IsEnabled="{Binding EnabledUserSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenDeviceSaveDirectory}"
IsEnabled="{Binding EnabledDeviceSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenDeviceSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenDeviceSaveDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenBcatSaveDirectory}"
IsEnabled="{Binding EnabledBcatSaveDirectory}"
Header="{locale:Locale GameListContextMenuOpenBcatSaveDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenBcatSaveDirectoryToolTip}" />
<Separator />
<MenuItem
Command="{Binding OpenTitleUpdateManager}"
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
<MenuItem
Command="{Binding OpenDownloadableContentManager}"
Header="{locale:Locale GameListContextMenuManageDlc}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
<MenuItem
Command="{Binding OpenCheatManager}"
Header="{locale:Locale GameListContextMenuManageCheat}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
<MenuItem
Command="{Binding OpenModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenSdModsDirectory}"
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
<Separator />
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
<MenuItem
Command="{Binding PurgePtcCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
<MenuItem
Command="{Binding PurgeShaderCache}"
Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
<MenuItem
Command="{Binding OpenPtcDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
<MenuItem
Command="{Binding OpenShaderCacheDirectory}"
Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
</MenuItem>
<MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
<MenuItem
Command="{Binding ExtractExeFs}"
Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
<MenuItem
Command="{Binding ExtractRomFs}"
Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Command="{Binding ExtractLogo}"
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
</MenuItem>
</MenuFlyout>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0"
Padding="8"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ContextFlyout="{StaticResource GameContextMenu}"
DoubleTapped="GameList_DoubleTapped"
Items="{Binding AppsObservableList}"
SelectionChanged="GameList_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<flex:FlexPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlignContent="FlexStart"
JustifyContent="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Border
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
ClipToBounds="True"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<Panel
Grid.Row="1"
Height="50"
Margin="0 10 0 0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}">
<TextBlock
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding TitleName}"
TextAlignment="Center"
TextWrapping="Wrap" />
</Panel>
</Grid>
</Border>
<ui:SymbolIcon
Margin="5,5,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
FontSize="16"
Foreground="{DynamicResource SystemAccentColor}"
IsVisible="{Binding Favorite}"
Symbol="StarFilled" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@@ -6,8 +6,6 @@ using Avalonia.Threading;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common; using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input; using Ryujinx.Ava.Input;
@@ -33,13 +31,11 @@ using SixLabors.ImageSharp.PixelFormats;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Path = System.IO.Path; using Path = System.IO.Path;
using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
using UserId = LibHac.Fs.UserId;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
@@ -346,11 +342,11 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public bool EnabledUserSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0; public bool OpenUserSaveDirectoryEnabled => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;
public bool EnabledDeviceSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; public bool OpenDeviceSaveDirectoryEnabled => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;
public bool EnabledBcatSaveDirectory => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; public bool OpenBcatSaveDirectoryEnabled => !Utilities.IsZeros(SelectedApplication.ControlHolder.ByteSpan) && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
public string LoadHeading public string LoadHeading
{ {
@@ -941,7 +937,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}; };
} }
private void RefreshView() public void RefreshView()
{ {
RefreshGrid(); RefreshGrid();
} }
@@ -1116,30 +1112,6 @@ namespace Ryujinx.Ava.UI.ViewModels
})); }));
} }
private async void ExtractLogo()
{
if (SelectedApplication != null)
{
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, SelectedApplication.Path, SelectedApplication.TitleName);
}
}
private async void ExtractRomFs()
{
if (SelectedApplication != null)
{
await ApplicationHelper.ExtractSection(NcaSectionType.Data, SelectedApplication.Path, SelectedApplication.TitleName);
}
}
private async void ExtractExeFs()
{
if (SelectedApplication != null)
{
await ApplicationHelper.ExtractSection(NcaSectionType.Code, SelectedApplication.Path, SelectedApplication.TitleName);
}
}
private void PrepareLoadScreen() private void PrepareLoadScreen()
{ {
using MemoryStream stream = new(SelectedIcon); using MemoryStream stream = new(SelectedIcon);
@@ -1383,241 +1355,11 @@ namespace Ryujinx.Ava.UI.ViewModels
await NavigationDialogHost.Show(AccountManager, ContentManager, VirtualFileSystem, LibHacHorizonManager.RyujinxClient); await NavigationDialogHost.Show(AccountManager, ContentManager, VirtualFileSystem, LibHacHorizonManager.RyujinxClient);
} }
public void OpenPtcDirectory()
{
ApplicationData selection = SelectedApplication;
if (selection != null)
{
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu");
string mainPath = Path.Combine(ptcDir, "0");
string backupPath = Path.Combine(ptcDir, "1");
if (!Directory.Exists(ptcDir))
{
Directory.CreateDirectory(ptcDir);
Directory.CreateDirectory(mainPath);
Directory.CreateDirectory(backupPath);
}
OpenHelper.OpenFolder(ptcDir);
}
}
public async void PurgePtcCache()
{
ApplicationData selection = SelectedApplication;
if (selection != null)
{
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "0"));
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "cpu", "1"));
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, selection.TitleName),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
List<FileInfo> cacheFiles = new();
if (mainDir.Exists)
{
cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache"));
}
if (backupDir.Exists)
{
cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache"));
}
if (cacheFiles.Count > 0 && result == UserResult.Yes)
{
foreach (FileInfo file in cacheFiles)
{
try
{
file.Delete();
}
catch (Exception e)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, e));
}
}
}
}
}
public void OpenShaderCacheDirectory()
{
ApplicationData selection = SelectedApplication;
if (selection != null)
{
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
Directory.CreateDirectory(shaderCacheDir);
}
OpenHelper.OpenFolder(shaderCacheDir);
}
}
public void SimulateWakeUpMessage() public void SimulateWakeUpMessage()
{ {
AppHost.Device.System.SimulateWakeUpMessage(); AppHost.Device.System.SimulateWakeUpMessage();
} }
public async void PurgeShaderCache()
{
ApplicationData selection = SelectedApplication;
if (selection != null)
{
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, selection.TitleId, "cache", "shader"));
// FIXME: Found a way to reproduce the bold effect on the title name (fork?).
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, selection.TitleName),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
List<DirectoryInfo> oldCacheDirectories = new();
List<FileInfo> newCacheFiles = new();
if (shaderCacheDir.Exists)
{
oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*"));
newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc"));
newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data"));
}
if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0) && result == UserResult.Yes)
{
foreach (DirectoryInfo directory in oldCacheDirectories)
{
try
{
directory.Delete(true);
}
catch (Exception e)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, directory.Name, e));
}
}
}
foreach (FileInfo file in newCacheFiles)
{
try
{
file.Delete();
}
catch (Exception e)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.ShaderCachePurgeError, file.Name, e));
}
}
}
}
public void ToggleFavorite()
{
ApplicationData selection = SelectedApplication;
if (selection != null)
{
selection.Favorite = !selection.Favorite;
ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata =>
{
appMetadata.Favorite = selection.Favorite;
});
RefreshView();
}
}
public void OpenUserSaveDirectory()
{
OpenSaveDirectory(SaveDataType.Account, userId: new UserId((ulong)AccountManager.LastOpenedUser.UserId.High, (ulong)AccountManager.LastOpenedUser.UserId.Low));
}
public void OpenDeviceSaveDirectory()
{
OpenSaveDirectory(SaveDataType.Device, userId: default);
}
public void OpenBcatSaveDirectory()
{
OpenSaveDirectory(SaveDataType.Bcat, userId: default);
}
private void OpenSaveDirectory(SaveDataType saveDataType, UserId userId)
{
if (SelectedApplication != null)
{
if (!ulong.TryParse(SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
});
return;
}
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, SelectedApplication.ControlHolder, SelectedApplication.TitleName);
}
}
public void OpenModsDirectory()
{
if (SelectedApplication != null)
{
string modsBasePath = VirtualFileSystem.ModLoader.GetModsBasePath();
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath);
}
}
public void OpenSdModsDirectory()
{
if (SelectedApplication != null)
{
string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath();
string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId);
OpenHelper.OpenFolder(titleModsPath);
}
}
public async void OpenTitleUpdateManager()
{
if (SelectedApplication != null)
{
await TitleUpdateWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
}
}
public async void OpenDownloadableContentManager()
{
if (SelectedApplication != null)
{
await DownloadableContentManagerWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
}
}
public async void OpenCheatManager()
{
if (SelectedApplication != null)
{
await new CheatWindow(VirtualFileSystem, SelectedApplication.TitleId, SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
}
}
public async void LoadApplications() public async void LoadApplications()
{ {
await Dispatcher.UIThread.InvokeAsync(() => await Dispatcher.UIThread.InvokeAsync(() =>

View File

@@ -35,8 +35,8 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); InitializeComponent();
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId);
ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber); ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
_enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt"); _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");

View File

@@ -88,16 +88,16 @@
<main:MainViewControls <main:MainViewControls
Name="ViewControls" Name="ViewControls"
Grid.Row="0"/> Grid.Row="0"/>
<controls:GameListView <controls:ApplicationListView
x:Name="GameList" x:Name="ApplicationList"
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
IsVisible="{Binding IsList}" /> IsVisible="{Binding IsList}" />
<controls:GameGridView <controls:ApplicationGridView
x:Name="GameGrid" x:Name="ApplicationGrid"
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"

View File

@@ -288,13 +288,13 @@ namespace Ryujinx.Ava.UI.Windows
{ {
StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged;
GameGrid.ApplicationOpened += Application_Opened; ApplicationGrid.ApplicationOpened += Application_Opened;
GameGrid.DataContext = ViewModel; ApplicationGrid.DataContext = ViewModel;
GameList.ApplicationOpened += Application_Opened; ApplicationList.ApplicationOpened += Application_Opened;
GameList.DataContext = ViewModel; ApplicationList.DataContext = ViewModel;
LoadHotKeys(); LoadHotKeys();
} }

View File

@@ -157,11 +157,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
{ {
BufferDescriptor sb = info.SBuffers[index]; BufferDescriptor sb = info.SBuffers[index];
ulong sbDescAddress = _channel.BufferManager.GetComputeUniformBufferAddress(0); ulong sbDescAddress = _channel.BufferManager.GetComputeUniformBufferAddress(sb.SbCbSlot);
sbDescAddress += (ulong)sb.SbCbOffset * 4;
int sbDescOffset = 0x310 + sb.Slot * 0x10;
sbDescAddress += (ulong)sbDescOffset;
SbDescriptor sbDescriptor = _channel.MemoryManager.Physical.Read<SbDescriptor>(sbDescAddress); SbDescriptor sbDescriptor = _channel.MemoryManager.Physical.Read<SbDescriptor>(sbDescAddress);

View File

@@ -351,11 +351,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{ {
BufferDescriptor sb = info.SBuffers[index]; BufferDescriptor sb = info.SBuffers[index];
ulong sbDescAddress = _channel.BufferManager.GetGraphicsUniformBufferAddress(stage, 0); ulong sbDescAddress = _channel.BufferManager.GetGraphicsUniformBufferAddress(stage, sb.SbCbSlot);
sbDescAddress += (ulong)sb.SbCbOffset * 4;
int sbDescOffset = 0x110 + stage * 0x100 + sb.Slot * 0x10;
sbDescAddress += (ulong)sbDescOffset;
SbDescriptor sbDescriptor = _channel.MemoryManager.Physical.Read<SbDescriptor>(sbDescAddress); SbDescriptor sbDescriptor = _channel.MemoryManager.Physical.Read<SbDescriptor>(sbDescAddress);

View File

@@ -300,11 +300,16 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
IsCopyRegionComplete(srcCopyTexture, srcCopyTextureFormat, srcX1, srcY1, srcX2, srcY2) && IsCopyRegionComplete(srcCopyTexture, srcCopyTextureFormat, srcX1, srcY1, srcX2, srcY2) &&
IsCopyRegionComplete(dstCopyTexture, dstCopyTextureFormat, dstX1, dstY1, dstX2, dstY2); IsCopyRegionComplete(dstCopyTexture, dstCopyTextureFormat, dstX1, dstY1, dstX2, dstY2);
// We can only allow aliasing of color formats as depth if the source and destination textures
// are the same, as we can't blit between different depth formats.
bool srcDepthAlias = srcCopyTexture.Format == dstCopyTexture.Format;
var srcTexture = memoryManager.Physical.TextureCache.FindOrCreateTexture( var srcTexture = memoryManager.Physical.TextureCache.FindOrCreateTexture(
memoryManager, memoryManager,
srcCopyTexture, srcCopyTexture,
offset, offset,
srcCopyTextureFormat, srcCopyTextureFormat,
srcDepthAlias,
!canDirectCopy, !canDirectCopy,
false, false,
srcHint); srcHint);
@@ -325,6 +330,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
// When the source texture that was found has a depth format, // When the source texture that was found has a depth format,
// we must enforce the target texture also has a depth format, // we must enforce the target texture also has a depth format,
// as copies between depth and color formats are not allowed. // as copies between depth and color formats are not allowed.
// For depth blit, the destination texture format should always match exactly.
if (srcTexture.Format.IsDepthOrStencil()) if (srcTexture.Format.IsDepthOrStencil())
{ {
@@ -340,7 +346,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Twod
dstCopyTexture, dstCopyTexture,
0, 0,
dstCopyTextureFormat, dstCopyTextureFormat,
true, depthAlias: false,
shouldCreate: true,
srcTexture.ScaleMode == TextureScaleMode.Scaled, srcTexture.ScaleMode == TextureScaleMode.Scaled,
dstHint); dstHint);

View File

@@ -1118,7 +1118,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
bool forSampler = (flags & TextureSearchFlags.ForSampler) != 0; bool forSampler = (flags & TextureSearchFlags.ForSampler) != 0;
TextureMatchQuality matchQuality = TextureCompatibility.FormatMatches(Info, info, forSampler, (flags & TextureSearchFlags.ForCopy) != 0); TextureMatchQuality matchQuality = TextureCompatibility.FormatMatches(Info, info, forSampler, (flags & TextureSearchFlags.DepthAlias) != 0);
if (matchQuality == TextureMatchQuality.NoMatch) if (matchQuality == TextureMatchQuality.NoMatch)
{ {

View File

@@ -537,7 +537,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (hostTexture != null && texture.Target == Target.TextureBuffer) if (hostTexture != null && texture.Target == Target.TextureBuffer)
{ {
// Ensure that the buffer texture is using the correct buffer as storage. // Ensure that the buffer texture is using the correct buffer as storage.
// Buffers are frequently re-created to accomodate larger data, so we need to re-bind // Buffers are frequently re-created to accommodate larger data, so we need to re-bind
// to ensure we're not using a old buffer that was already deleted. // to ensure we're not using a old buffer that was already deleted.
_channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, bindingInfo.Format, false); _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, bindingInfo.Format, false);
@@ -666,7 +666,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (hostTexture != null && texture.Target == Target.TextureBuffer) if (hostTexture != null && texture.Target == Target.TextureBuffer)
{ {
// Ensure that the buffer texture is using the correct buffer as storage. // Ensure that the buffer texture is using the correct buffer as storage.
// Buffers are frequently re-created to accomodate larger data, so we need to re-bind // Buffers are frequently re-created to accommodate larger data, so we need to re-bind
// to ensure we're not using a old buffer that was already deleted. // to ensure we're not using a old buffer that was already deleted.
Format format = bindingInfo.Format; Format format = bindingInfo.Format;
@@ -879,4 +879,4 @@ namespace Ryujinx.Graphics.Gpu.Image
Array.Clear(_imageState); Array.Clear(_imageState);
} }
} }
} }

View File

@@ -249,6 +249,8 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="copyTexture">Copy texture to find or create</param> /// <param name="copyTexture">Copy texture to find or create</param>
/// <param name="offset">Offset to be added to the physical texture address</param> /// <param name="offset">Offset to be added to the physical texture address</param>
/// <param name="formatInfo">Format information of the copy texture</param> /// <param name="formatInfo">Format information of the copy texture</param>
/// <param name="depthAlias">Indicates if aliasing between color and depth format should be allowed</param>
/// <param name="shouldCreate">Indicates if a new texture should be created if none is found on the cache</param>
/// <param name="preferScaling">Indicates if the texture should be scaled from the start</param> /// <param name="preferScaling">Indicates if the texture should be scaled from the start</param>
/// <param name="sizeHint">A hint indicating the minimum used size for the texture</param> /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param>
/// <returns>The texture</returns> /// <returns>The texture</returns>
@@ -257,6 +259,7 @@ namespace Ryujinx.Graphics.Gpu.Image
TwodTexture copyTexture, TwodTexture copyTexture,
ulong offset, ulong offset,
FormatInfo formatInfo, FormatInfo formatInfo,
bool depthAlias,
bool shouldCreate, bool shouldCreate,
bool preferScaling, bool preferScaling,
Size sizeHint) Size sizeHint)
@@ -293,6 +296,11 @@ namespace Ryujinx.Graphics.Gpu.Image
TextureSearchFlags flags = TextureSearchFlags.ForCopy; TextureSearchFlags flags = TextureSearchFlags.ForCopy;
if (depthAlias)
{
flags |= TextureSearchFlags.DepthAlias;
}
if (preferScaling) if (preferScaling)
{ {
flags |= TextureSearchFlags.WithUpscale; flags |= TextureSearchFlags.WithUpscale;

View File

@@ -220,18 +220,18 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="lhs">Texture information to compare</param> /// <param name="lhs">Texture information to compare</param>
/// <param name="rhs">Texture information to compare with</param> /// <param name="rhs">Texture information to compare with</param>
/// <param name="forSampler">Indicates that the texture will be used for shader sampling</param> /// <param name="forSampler">Indicates that the texture will be used for shader sampling</param>
/// <param name="forCopy">Indicates that the texture will be used as copy source or target</param> /// <param name="depthAlias">Indicates if aliasing between color and depth format should be allowed</param>
/// <returns>A value indicating how well the formats match</returns> /// <returns>A value indicating how well the formats match</returns>
public static TextureMatchQuality FormatMatches(TextureInfo lhs, TextureInfo rhs, bool forSampler, bool forCopy) public static TextureMatchQuality FormatMatches(TextureInfo lhs, TextureInfo rhs, bool forSampler, bool depthAlias)
{ {
// D32F and R32F texture have the same representation internally, // D32F and R32F texture have the same representation internally,
// however the R32F format is used to sample from depth textures. // however the R32F format is used to sample from depth textures.
if (lhs.FormatInfo.Format == Format.D32Float && rhs.FormatInfo.Format == Format.R32Float && (forSampler || forCopy)) if (lhs.FormatInfo.Format == Format.D32Float && rhs.FormatInfo.Format == Format.R32Float && (forSampler || depthAlias))
{ {
return TextureMatchQuality.FormatAlias; return TextureMatchQuality.FormatAlias;
} }
if (forCopy) if (depthAlias)
{ {
// The 2D engine does not support depth-stencil formats, so it will instead // The 2D engine does not support depth-stencil formats, so it will instead
// use equivalent color formats. We must also consider them as compatible. // use equivalent color formats. We must also consider them as compatible.

View File

@@ -11,7 +11,8 @@ namespace Ryujinx.Graphics.Gpu.Image
None = 0, None = 0,
ForSampler = 1 << 1, ForSampler = 1 << 1,
ForCopy = 1 << 2, ForCopy = 1 << 2,
WithUpscale = 1 << 3, DepthAlias = 1 << 3,
NoCreate = 1 << 4 WithUpscale = 1 << 4,
NoCreate = 1 << 5
} }
} }

View File

@@ -68,6 +68,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
private int _referenceCount = 1; private int _referenceCount = 1;
private ulong _dirtyStart = ulong.MaxValue;
private ulong _dirtyEnd = ulong.MaxValue;
/// <summary> /// <summary>
/// Creates a new instance of the buffer. /// Creates a new instance of the buffer.
/// </summary> /// </summary>
@@ -221,6 +224,26 @@ namespace Ryujinx.Graphics.Gpu.Memory
} }
_sequenceNumber = _context.SequenceNumber; _sequenceNumber = _context.SequenceNumber;
_dirtyStart = ulong.MaxValue;
}
}
if (_dirtyStart != ulong.MaxValue)
{
ulong end = address + size;
if (end > _dirtyStart && address < _dirtyEnd)
{
if (_modifiedRanges != null)
{
_modifiedRanges.ExcludeModifiedRegions(_dirtyStart, _dirtyEnd - _dirtyStart, _loadDelegate);
}
else
{
LoadRegion(_dirtyStart, _dirtyEnd - _dirtyStart);
}
_dirtyStart = ulong.MaxValue;
} }
} }
} }
@@ -291,7 +314,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
} }
/// <summary> /// <summary>
/// Inherit modified ranges from another buffer. /// Inherit modified and dirty ranges from another buffer.
/// </summary> /// </summary>
/// <param name="from">The buffer to inherit from</param> /// <param name="from">The buffer to inherit from</param>
public void InheritModifiedRanges(Buffer from) public void InheritModifiedRanges(Buffer from)
@@ -320,6 +343,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges.InheritRanges(from._modifiedRanges, registerRangeAction); _modifiedRanges.InheritRanges(from._modifiedRanges, registerRangeAction);
} }
if (from._dirtyStart != ulong.MaxValue)
{
ForceDirty(from._dirtyStart, from._dirtyEnd - from._dirtyStart);
}
} }
/// <summary> /// <summary>
@@ -338,6 +366,44 @@ namespace Ryujinx.Graphics.Gpu.Memory
return false; return false;
} }
/// <summary>
/// Clear the dirty range that overlaps with the given region.
/// </summary>
/// <param name="address">Start address of the modified region</param>
/// <param name="size">Size of the modified region</param>
private void ClearDirty(ulong address, ulong size)
{
if (_dirtyStart != ulong.MaxValue)
{
ulong end = address + size;
if (end > _dirtyStart && address < _dirtyEnd)
{
if (address <= _dirtyStart)
{
// Cut off the start.
if (end < _dirtyEnd)
{
_dirtyStart = end;
}
else
{
_dirtyStart = ulong.MaxValue;
}
}
else if (end >= _dirtyEnd)
{
// Cut off the end.
_dirtyEnd = address;
}
// If fully contained, do nothing.
}
}
}
/// <summary> /// <summary>
/// Indicate that a region of the buffer was modified, and must be loaded from memory. /// Indicate that a region of the buffer was modified, and must be loaded from memory.
/// </summary> /// </summary>
@@ -357,6 +423,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
mSize = maxSize; mSize = maxSize;
} }
ClearDirty(mAddress, mSize);
if (_modifiedRanges != null) if (_modifiedRanges != null)
{ {
_modifiedRanges.ExcludeModifiedRegions(mAddress, mSize, _loadDelegate); _modifiedRanges.ExcludeModifiedRegions(mAddress, mSize, _loadDelegate);
@@ -380,14 +448,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
} }
/// <summary> /// <summary>
/// Force a region of the buffer to be dirty. Avoids reprotection and nullifies sequence number check. /// Force a region of the buffer to be dirty within the memory tracking. Avoids reprotection and nullifies sequence number check.
/// </summary> /// </summary>
/// <param name="mAddress">Start address of the modified region</param> /// <param name="mAddress">Start address of the modified region</param>
/// <param name="mSize">Size of the region to force dirty</param> /// <param name="mSize">Size of the region to force dirty</param>
public void ForceDirty(ulong mAddress, ulong mSize) private void ForceTrackingDirty(ulong mAddress, ulong mSize)
{ {
_modifiedRanges?.Clear(mAddress, mSize);
if (_useGranular) if (_useGranular)
{ {
_memoryTrackingGranular.ForceDirty(mAddress, mSize); _memoryTrackingGranular.ForceDirty(mAddress, mSize);
@@ -399,6 +465,39 @@ namespace Ryujinx.Graphics.Gpu.Memory
} }
} }
/// <summary>
/// Force a region of the buffer to be dirty. Avoids reprotection and nullifies sequence number check.
/// </summary>
/// <param name="mAddress">Start address of the modified region</param>
/// <param name="mSize">Size of the region to force dirty</param>
public void ForceDirty(ulong mAddress, ulong mSize)
{
_modifiedRanges?.Clear(mAddress, mSize);
ulong end = mAddress + mSize;
if (_dirtyStart == ulong.MaxValue)
{
_dirtyStart = mAddress;
_dirtyEnd = end;
}
else
{
// Is the new range more than a page away from the existing one?
if ((long)(mAddress - _dirtyEnd) >= (long)MemoryManager.PageSize ||
(long)(_dirtyStart - end) >= (long)MemoryManager.PageSize)
{
ForceTrackingDirty(mAddress, mSize);
}
else
{
_dirtyStart = Math.Min(_dirtyStart, mAddress);
_dirtyEnd = Math.Max(_dirtyEnd, end);
}
}
}
/// <summary> /// <summary>
/// Performs copy of all the buffer data from one buffer to another. /// Performs copy of all the buffer data from one buffer to another.
/// </summary> /// </summary>

View File

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

View File

@@ -104,7 +104,7 @@ namespace Ryujinx.Graphics.OpenGL
private static GpuVendor GetGpuVendor() private static GpuVendor GetGpuVendor()
{ {
string vendor = GL.GetString(StringName.Vendor).ToLower(); string vendor = GL.GetString(StringName.Vendor).ToLowerInvariant();
if (vendor == "nvidia corporation") if (vendor == "nvidia corporation")
{ {
@@ -112,7 +112,7 @@ namespace Ryujinx.Graphics.OpenGL
} }
else if (vendor == "intel") else if (vendor == "intel")
{ {
string renderer = GL.GetString(StringName.Renderer).ToLower(); string renderer = GL.GetString(StringName.Renderer).ToLowerInvariant();
return renderer.Contains("mesa") ? GpuVendor.IntelUnix : GpuVendor.IntelWindows; return renderer.Contains("mesa") ? GpuVendor.IntelUnix : GpuVendor.IntelWindows;
} }

View File

@@ -5,13 +5,27 @@ namespace Ryujinx.Graphics.Shader
// New fields should be added to the end of the struct to keep disk shader cache compatibility. // New fields should be added to the end of the struct to keep disk shader cache compatibility.
public readonly int Binding; public readonly int Binding;
public readonly int Slot; public readonly byte Slot;
public readonly byte SbCbSlot;
public readonly ushort SbCbOffset;
public BufferUsageFlags Flags; public BufferUsageFlags Flags;
public BufferDescriptor(int binding, int slot) public BufferDescriptor(int binding, int slot)
{ {
Binding = binding; Binding = binding;
Slot = slot; Slot = (byte)slot;
SbCbSlot = 0;
SbCbOffset = 0;
Flags = BufferUsageFlags.None;
}
public BufferDescriptor(int binding, int slot, int sbCbSlot, int sbCbOffset)
{
Binding = binding;
Slot = (byte)slot;
SbCbSlot = (byte)sbCbSlot;
SbCbOffset = (ushort)sbCbOffset;
Flags = BufferUsageFlags.None; Flags = BufferUsageFlags.None;
} }

View File

@@ -6,7 +6,7 @@ namespace Ryujinx.Graphics.Shader
/// Flags that indicate how a buffer will be used in a shader. /// Flags that indicate how a buffer will be used in a shader.
/// </summary> /// </summary>
[Flags] [Flags]
public enum BufferUsageFlags public enum BufferUsageFlags : byte
{ {
None = 0, None = 0,

View File

@@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.Shader.Translation
public const int UbeDescsSize = StorageDescSize * UbeMaxCount; public const int UbeDescsSize = StorageDescSize * UbeMaxCount;
public const int UbeFirstCbuf = 8; public const int UbeFirstCbuf = 8;
public const int DriverReservedCb = 0;
public static bool UsesGlobalMemory(Instruction inst, StorageKind storageKind) public static bool UsesGlobalMemory(Instruction inst, StorageKind storageKind)
{ {
return (inst.IsAtomic() && storageKind == StorageKind.GlobalMemory) || return (inst.IsAtomic() && storageKind == StorageKind.GlobalMemory) ||

View File

@@ -8,6 +8,20 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
{ {
static class GlobalToStorage static class GlobalToStorage
{ {
private struct SearchResult
{
public static SearchResult NotFound => new SearchResult(-1, 0);
public bool Found => SbCbSlot != -1;
public int SbCbSlot { get; }
public int SbCbOffset { get; }
public SearchResult(int sbCbSlot, int sbCbOffset)
{
SbCbSlot = sbCbSlot;
SbCbOffset = sbCbOffset;
}
}
public static void RunPass(BasicBlock block, ShaderConfig config, ref int sbUseMask, ref int ubeUseMask) public static void RunPass(BasicBlock block, ShaderConfig config, ref int sbUseMask, ref int ubeUseMask)
{ {
int sbStart = GetStorageBaseCbOffset(config.Stage); int sbStart = GetStorageBaseCbOffset(config.Stage);
@@ -49,30 +63,33 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
{ {
Operand source = operation.GetSource(0); Operand source = operation.GetSource(0);
int storageIndex = SearchForStorageBase(block, source, sbStart, sbEnd); var result = SearchForStorageBase(config, block, source);
if (!result.Found)
if (storageIndex >= 0)
{ {
// Storage buffers are implemented using global memory access. continue;
// If we know from where the base address of the access is loaded,
// we can guess which storage buffer it is accessing.
// We can then replace the global memory access with a storage
// buffer access.
node = ReplaceGlobalWithStorage(block, node, config, storageIndex);
} }
else if (config.Stage == ShaderStage.Compute && operation.Inst == Instruction.LoadGlobal)
if (config.Stage == ShaderStage.Compute &&
operation.Inst == Instruction.LoadGlobal &&
result.SbCbSlot == DriverReservedCb &&
result.SbCbOffset >= UbeBaseOffset &&
result.SbCbOffset < UbeBaseOffset + UbeDescsSize)
{ {
// Here we effectively try to replace a LDG instruction with LDC. // Here we effectively try to replace a LDG instruction with LDC.
// The hardware only supports a limited amount of constant buffers // The hardware only supports a limited amount of constant buffers
// so NVN "emulates" more constant buffers using global memory access. // so NVN "emulates" more constant buffers using global memory access.
// Here we try to replace the global access back to a constant buffer // Here we try to replace the global access back to a constant buffer
// load. // load.
storageIndex = SearchForStorageBase(block, source, ubeStart, ubeStart + ubeEnd); node = ReplaceLdgWithLdc(node, config, (result.SbCbOffset - UbeBaseOffset) / StorageDescSize);
}
if (storageIndex >= 0) else
{ {
node = ReplaceLdgWithLdc(node, config, storageIndex); // Storage buffers are implemented using global memory access.
} // If we know from where the base address of the access is loaded,
// we can guess which storage buffer it is accessing.
// We can then replace the global memory access with a storage
// buffer access.
node = ReplaceGlobalWithStorage(block, node, config, config.GetSbSlot((byte)result.SbCbSlot, (ushort)result.SbCbOffset));
} }
} }
} }
@@ -159,7 +176,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
if (byteOffset == null) if (byteOffset == null)
{ {
Operand baseAddrLow = Cbuf(0, baseAddressCbOffset); (int sbCbSlot, int sbCbOffset) = config.GetSbCbInfo(storageIndex);
Operand baseAddrLow = Cbuf(sbCbSlot, sbCbOffset);
Operand baseAddrTrunc = Local(); Operand baseAddrTrunc = Local();
Operand alignMask = Const(-config.GpuAccessor.QueryHostStorageBufferOffsetAlignment()); Operand alignMask = Const(-config.GpuAccessor.QueryHostStorageBufferOffsetAlignment());
@@ -360,20 +379,20 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return node; return node;
} }
private static int SearchForStorageBase(BasicBlock block, Operand globalAddress, int sbStart, int sbEnd) private static SearchResult SearchForStorageBase(ShaderConfig config, BasicBlock block, Operand globalAddress)
{ {
globalAddress = Utils.FindLastOperation(globalAddress, block); globalAddress = Utils.FindLastOperation(globalAddress, block);
if (globalAddress.Type == OperandType.ConstantBuffer) if (globalAddress.Type == OperandType.ConstantBuffer)
{ {
return GetStorageIndex(globalAddress, sbStart, sbEnd); return GetStorageIndex(config, globalAddress);
} }
Operation operation = globalAddress.AsgOp as Operation; Operation operation = globalAddress.AsgOp as Operation;
if (operation == null || operation.Inst != Instruction.Add) if (operation == null || operation.Inst != Instruction.Add)
{ {
return -1; return SearchResult.NotFound;
} }
Operand src1 = operation.GetSource(0); Operand src1 = operation.GetSource(0);
@@ -382,34 +401,65 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
if ((src1.Type == OperandType.LocalVariable && src2.Type == OperandType.Constant) || if ((src1.Type == OperandType.LocalVariable && src2.Type == OperandType.Constant) ||
(src2.Type == OperandType.LocalVariable && src1.Type == OperandType.Constant)) (src2.Type == OperandType.LocalVariable && src1.Type == OperandType.Constant))
{ {
Operand baseAddr;
if (src1.Type == OperandType.LocalVariable) if (src1.Type == OperandType.LocalVariable)
{ {
operation = Utils.FindLastOperation(src1, block).AsgOp as Operation; baseAddr = Utils.FindLastOperation(src1, block);
} }
else else
{ {
operation = Utils.FindLastOperation(src2, block).AsgOp as Operation; baseAddr = Utils.FindLastOperation(src2, block);
} }
var result = GetStorageIndex(config, baseAddr);
if (result.Found)
{
return result;
}
operation = baseAddr.AsgOp as Operation;
if (operation == null || operation.Inst != Instruction.Add) if (operation == null || operation.Inst != Instruction.Add)
{ {
return -1; return SearchResult.NotFound;
} }
} }
var selectedResult = SearchResult.NotFound;
for (int index = 0; index < operation.SourcesCount; index++) for (int index = 0; index < operation.SourcesCount; index++)
{ {
Operand source = operation.GetSource(index); Operand source = operation.GetSource(index);
int storageIndex = GetStorageIndex(source, sbStart, sbEnd); var result = GetStorageIndex(config, source);
if (storageIndex != -1) // If we already have a result, we give preference to the ones from
// the driver reserved constant buffer, as those are the ones that
// contains the base address.
if (result.Found && (!selectedResult.Found || result.SbCbSlot == GlobalMemory.DriverReservedCb))
{ {
return storageIndex; selectedResult = result;
} }
} }
return -1; return selectedResult;
}
private static SearchResult GetStorageIndex(ShaderConfig config, Operand operand)
{
if (operand.Type == OperandType.ConstantBuffer)
{
int slot = operand.GetCbufSlot();
int offset = operand.GetCbufOffset();
if ((offset & 3) == 0)
{
return new SearchResult(slot, offset);
}
}
return SearchResult.NotFound;
} }
private static int GetStorageIndex(Operand operand, int sbStart, int sbEnd) private static int GetStorageIndex(Operand operand, int sbStart, int sbEnd)

View File

@@ -68,7 +68,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
} }
ConstantFolding.RunPass(operation); ConstantFolding.RunPass(operation);
Simplification.RunPass(operation); Simplification.RunPass(operation);
if (DestIsLocalVar(operation)) if (DestIsLocalVar(operation))

View File

@@ -110,9 +110,9 @@ namespace Ryujinx.Graphics.Shader.Translation
Operand BindingRangeCheck(int cbOffset, out Operand baseAddrLow) Operand BindingRangeCheck(int cbOffset, out Operand baseAddrLow)
{ {
baseAddrLow = Cbuf(0, cbOffset); baseAddrLow = Cbuf(DriverReservedCb, cbOffset);
Operand baseAddrHigh = Cbuf(0, cbOffset + 1); Operand baseAddrHigh = Cbuf(DriverReservedCb, cbOffset + 1);
Operand size = Cbuf(0, cbOffset + 2); Operand size = Cbuf(DriverReservedCb, cbOffset + 2);
Operand offset = PrependOperation(Instruction.Subtract, addrLow, baseAddrLow); Operand offset = PrependOperation(Instruction.Subtract, addrLow, baseAddrLow);
Operand borrow = PrependOperation(Instruction.CompareLessU32, addrLow, baseAddrLow); Operand borrow = PrependOperation(Instruction.CompareLessU32, addrLow, baseAddrLow);
@@ -134,9 +134,10 @@ namespace Ryujinx.Graphics.Shader.Translation
sbUseMask &= ~(1 << slot); sbUseMask &= ~(1 << slot);
config.SetUsedStorageBuffer(slot, isWrite);
int cbOffset = GetStorageCbOffset(config.Stage, slot); int cbOffset = GetStorageCbOffset(config.Stage, slot);
slot = config.GetSbSlot(DriverReservedCb, (ushort)cbOffset);
config.SetUsedStorageBuffer(slot, isWrite);
Operand inRange = BindingRangeCheck(cbOffset, out Operand baseAddrLow); Operand inRange = BindingRangeCheck(cbOffset, out Operand baseAddrLow);

View File

@@ -125,6 +125,9 @@ namespace Ryujinx.Graphics.Shader.Translation
private readonly Dictionary<TextureInfo, TextureMeta> _usedTextures; private readonly Dictionary<TextureInfo, TextureMeta> _usedTextures;
private readonly Dictionary<TextureInfo, TextureMeta> _usedImages; private readonly Dictionary<TextureInfo, TextureMeta> _usedImages;
private readonly Dictionary<int, int> _sbSlots;
private readonly Dictionary<int, int> _sbSlotsReverse;
private BufferDescriptor[] _cachedConstantBufferDescriptors; private BufferDescriptor[] _cachedConstantBufferDescriptors;
private BufferDescriptor[] _cachedStorageBufferDescriptors; private BufferDescriptor[] _cachedStorageBufferDescriptors;
private TextureDescriptor[] _cachedTextureDescriptors; private TextureDescriptor[] _cachedTextureDescriptors;
@@ -152,6 +155,9 @@ namespace Ryujinx.Graphics.Shader.Translation
_usedTextures = new Dictionary<TextureInfo, TextureMeta>(); _usedTextures = new Dictionary<TextureInfo, TextureMeta>();
_usedImages = new Dictionary<TextureInfo, TextureMeta>(); _usedImages = new Dictionary<TextureInfo, TextureMeta>();
_sbSlots = new Dictionary<int, int>();
_sbSlotsReverse = new Dictionary<int, int>();
} }
public ShaderConfig( public ShaderConfig(
@@ -770,9 +776,8 @@ namespace Ryujinx.Graphics.Shader.Translation
usedMask |= (int)GpuAccessor.QueryConstantBufferUse(); usedMask |= (int)GpuAccessor.QueryConstantBufferUse();
} }
return _cachedConstantBufferDescriptors = GetBufferDescriptors( return _cachedConstantBufferDescriptors = GetUniformBufferDescriptors(
usedMask, usedMask,
0,
UsedFeatures.HasFlag(FeatureFlags.CbIndexing), UsedFeatures.HasFlag(FeatureFlags.CbIndexing),
out _firstConstantBufferBinding, out _firstConstantBufferBinding,
GpuAccessor.QueryBindingConstantBuffer); GpuAccessor.QueryBindingConstantBuffer);
@@ -785,7 +790,7 @@ namespace Ryujinx.Graphics.Shader.Translation
return _cachedStorageBufferDescriptors; return _cachedStorageBufferDescriptors;
} }
return _cachedStorageBufferDescriptors = GetBufferDescriptors( return _cachedStorageBufferDescriptors = GetStorageBufferDescriptors(
_usedStorageBuffers, _usedStorageBuffers,
_usedStorageBuffersWrite, _usedStorageBuffersWrite,
true, true,
@@ -793,7 +798,48 @@ namespace Ryujinx.Graphics.Shader.Translation
GpuAccessor.QueryBindingStorageBuffer); GpuAccessor.QueryBindingStorageBuffer);
} }
private static BufferDescriptor[] GetBufferDescriptors( private static BufferDescriptor[] GetUniformBufferDescriptors(int usedMask, bool isArray, out int firstBinding, Func<int, int> getBindingCallback)
{
firstBinding = 0;
int lastSlot = -1;
bool hasFirstBinding = false;
var descriptors = new BufferDescriptor[BitOperations.PopCount((uint)usedMask)];
for (int i = 0; i < descriptors.Length; i++)
{
int slot = BitOperations.TrailingZeroCount(usedMask);
if (isArray)
{
// The next array entries also consumes bindings, even if they are unused.
for (int j = lastSlot + 1; j < slot; j++)
{
int binding = getBindingCallback(j);
if (!hasFirstBinding)
{
firstBinding = binding;
hasFirstBinding = true;
}
}
}
lastSlot = slot;
descriptors[i] = new BufferDescriptor(getBindingCallback(slot), slot);
if (!hasFirstBinding)
{
firstBinding = descriptors[i].Binding;
hasFirstBinding = true;
}
usedMask &= ~(1 << slot);
}
return descriptors;
}
private BufferDescriptor[] GetStorageBufferDescriptors(
int usedMask, int usedMask,
int writtenMask, int writtenMask,
bool isArray, bool isArray,
@@ -827,7 +873,9 @@ namespace Ryujinx.Graphics.Shader.Translation
lastSlot = slot; lastSlot = slot;
descriptors[i] = new BufferDescriptor(getBindingCallback(slot), slot); (int sbCbSlot, int sbCbOffset) = GetSbCbInfo(slot);
descriptors[i] = new BufferDescriptor(getBindingCallback(slot), slot, sbCbSlot, sbCbOffset);
if (!hasFirstBinding) if (!hasFirstBinding)
{ {
@@ -924,6 +972,40 @@ namespace Ryujinx.Graphics.Shader.Translation
return FindDescriptorIndex(GetImageDescriptors(), texOp); return FindDescriptorIndex(GetImageDescriptors(), texOp);
} }
public int GetSbSlot(byte sbCbSlot, ushort sbCbOffset)
{
int key = PackSbCbInfo(sbCbSlot, sbCbOffset);
if (!_sbSlots.TryGetValue(key, out int slot))
{
slot = _sbSlots.Count;
_sbSlots.Add(key, slot);
_sbSlotsReverse.Add(slot, key);
}
return slot;
}
public (int, int) GetSbCbInfo(int slot)
{
if (_sbSlotsReverse.TryGetValue(slot, out int key))
{
return UnpackSbCbInfo(key);
}
throw new ArgumentException($"Invalid slot {slot}.", nameof(slot));
}
private static int PackSbCbInfo(int sbCbSlot, int sbCbOffset)
{
return sbCbOffset | ((int)sbCbSlot << 16);
}
private static (int, int) UnpackSbCbInfo(int key)
{
return ((byte)(key >> 16), (ushort)key);
}
public ShaderProgramInfo CreateProgramInfo(ShaderIdentification identification = ShaderIdentification.None) public ShaderProgramInfo CreateProgramInfo(ShaderIdentification identification = ShaderIdentification.None)
{ {
return new ShaderProgramInfo( return new ShaderProgramInfo(

View File

@@ -1015,8 +1015,8 @@ namespace Ryujinx.Graphics.Vulkan
private void SetRenderTargetsInternal(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked) private void SetRenderTargetsInternal(ITexture[] colors, ITexture depthStencil, bool filterWriteMasked)
{ {
FramebufferParams?.UpdateModifications();
CreateFramebuffer(colors, depthStencil, filterWriteMasked); CreateFramebuffer(colors, depthStencil, filterWriteMasked);
FramebufferParams?.UpdateModifications();
CreateRenderPass(); CreateRenderPass();
SignalStateChange(); SignalStateChange();
SignalAttachmentChange(); SignalAttachmentChange();

View File

@@ -89,7 +89,7 @@ namespace Ryujinx.HLE.HOS
} }
// Title independent mods // Title independent mods
public class PatchCache private class PatchCache
{ {
public List<Mod<DirectoryInfo>> NsoPatches { get; } public List<Mod<DirectoryInfo>> NsoPatches { get; }
public List<Mod<DirectoryInfo>> NroPatches { get; } public List<Mod<DirectoryInfo>> NroPatches { get; }
@@ -107,14 +107,14 @@ namespace Ryujinx.HLE.HOS
} }
} }
public Dictionary<ulong, ModCache> AppMods; // key is TitleId private readonly Dictionary<ulong, ModCache> _appMods; // key is TitleId
public PatchCache Patches; private PatchCache _patches;
private static readonly EnumerationOptions _dirEnumOptions; private static readonly EnumerationOptions DirEnumOptions;
static ModLoader() static ModLoader()
{ {
_dirEnumOptions = new EnumerationOptions DirEnumOptions = new EnumerationOptions
{ {
MatchCasing = MatchCasing.CaseInsensitive, MatchCasing = MatchCasing.CaseInsensitive,
MatchType = MatchType.Simple, MatchType = MatchType.Simple,
@@ -125,37 +125,73 @@ namespace Ryujinx.HLE.HOS
public ModLoader() public ModLoader()
{ {
AppMods = new Dictionary<ulong, ModCache>(); _appMods = new Dictionary<ulong, ModCache>();
Patches = new PatchCache(); _patches = new PatchCache();
} }
public void Clear() private void Clear()
{ {
AppMods.Clear(); _appMods.Clear();
Patches = new PatchCache(); _patches = new PatchCache();
} }
private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
public string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath()); public static string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath());
public string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath()); public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath());
private string EnsureBaseDirStructure(string modsBasePath) private static string EnsureBaseDirStructure(string modsBasePath)
{ {
var modsDir = new DirectoryInfo(modsBasePath); var modsDir = new DirectoryInfo(modsBasePath);
modsDir.CreateSubdirectory(AmsContentsDir); modsDir.CreateSubdirectory(AmsContentsDir);
modsDir.CreateSubdirectory(AmsNsoPatchDir); modsDir.CreateSubdirectory(AmsNsoPatchDir);
modsDir.CreateSubdirectory(AmsNroPatchDir); modsDir.CreateSubdirectory(AmsNroPatchDir);
// modsDir.CreateSubdirectory(AmsKipPatchDir); // uncomment when KIPs are supported // TODO: uncomment when KIPs are supported
// modsDir.CreateSubdirectory(AmsKipPatchDir);
return modsDir.FullName; return modsDir.FullName;
} }
private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId) private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId)
=> contentsDir.EnumerateDirectories($"{titleId}*", _dirEnumOptions).FirstOrDefault(); => contentsDir.EnumerateDirectories($"{titleId}*", DirEnumOptions).FirstOrDefault();
public string GetTitleDir(string modsBasePath, string titleId) private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, string titleId)
{
System.Text.StringBuilder types = new();
foreach (var modDir in dir.EnumerateDirectories())
{
types.Clear();
Mod<DirectoryInfo> mod = new("", null);
if (StrEquals(RomfsDir, modDir.Name))
{
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} RomFs>", modDir));
types.Append('R');
}
else if (StrEquals(ExefsDir, modDir.Name))
{
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} ExeFs>", modDir));
types.Append('E');
}
else if (StrEquals(CheatDir, modDir.Name))
{
types.Append('C', QueryCheatsDir(mods, modDir));
}
else
{
AddModsFromDirectory(mods, modDir, titleId);
}
if (types.Length > 0)
{
Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]");
}
}
}
public static string GetTitleDir(string modsBasePath, string titleId)
{ {
var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir)); var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir));
var titleModsPath = FindTitleDir(contentsDir, titleId); var titleModsPath = FindTitleDir(contentsDir, titleId);
@@ -170,17 +206,32 @@ namespace Ryujinx.HLE.HOS
} }
// Static Query Methods // Static Query Methods
public static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir) private static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir)
{ {
if (cache.Initialized || !patchDir.Exists) return; if (cache.Initialized || !patchDir.Exists)
{
return;
}
var patches = cache.KipPatches; List<Mod<DirectoryInfo>> patches;
string type = null; string type;
if (StrEquals(AmsNsoPatchDir, patchDir.Name)) { patches = cache.NsoPatches; type = "NSO"; } if (StrEquals(AmsNsoPatchDir, patchDir.Name))
else if (StrEquals(AmsNroPatchDir, patchDir.Name)) { patches = cache.NroPatches; type = "NRO"; } {
else if (StrEquals(AmsKipPatchDir, patchDir.Name)) { patches = cache.KipPatches; type = "KIP"; } patches = cache.NsoPatches; type = "NSO";
else return; }
else if (StrEquals(AmsNroPatchDir, patchDir.Name))
{
patches = cache.NroPatches; type = "NRO";
}
else if (StrEquals(AmsKipPatchDir, patchDir.Name))
{
patches = cache.KipPatches; type = "KIP";
}
else
{
return;
}
foreach (var modDir in patchDir.EnumerateDirectories()) foreach (var modDir in patchDir.EnumerateDirectories())
{ {
@@ -189,9 +240,12 @@ namespace Ryujinx.HLE.HOS
} }
} }
public static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir) private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir)
{ {
if (!titleDir.Exists) return; if (!titleDir.Exists)
{
return;
}
var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer));
if (fsFile.Exists) if (fsFile.Exists)
@@ -205,64 +259,15 @@ namespace Ryujinx.HLE.HOS
mods.ExefsContainers.Add(new Mod<FileInfo>($"<{titleDir.Name} ExeFs>", fsFile)); mods.ExefsContainers.Add(new Mod<FileInfo>($"<{titleDir.Name} ExeFs>", fsFile));
} }
System.Text.StringBuilder types = new System.Text.StringBuilder(5); AddModsFromDirectory(mods, titleDir, titleDir.Name);
foreach (var modDir in titleDir.EnumerateDirectories())
{
types.Clear();
Mod<DirectoryInfo> mod = new Mod<DirectoryInfo>("", null);
if (StrEquals(RomfsDir, modDir.Name))
{
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} RomFs>", modDir));
types.Append('R');
}
else if (StrEquals(ExefsDir, modDir.Name))
{
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} ExeFs>", modDir));
types.Append('E');
}
else if (StrEquals(CheatDir, modDir.Name))
{
for (int i = 0; i < QueryCheatsDir(mods, modDir); i++)
{
types.Append('C');
}
}
else
{
var romfs = new DirectoryInfo(Path.Combine(modDir.FullName, RomfsDir));
var exefs = new DirectoryInfo(Path.Combine(modDir.FullName, ExefsDir));
var cheat = new DirectoryInfo(Path.Combine(modDir.FullName, CheatDir));
if (romfs.Exists)
{
mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, romfs));
types.Append('R');
}
if (exefs.Exists)
{
mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, exefs));
types.Append('E');
}
if (cheat.Exists)
{
for (int i = 0; i < QueryCheatsDir(mods, cheat); i++)
{
types.Append('C');
}
}
}
if (types.Length > 0) Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]");
}
} }
public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId) public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId)
{ {
if (!contentsDir.Exists) return; if (!contentsDir.Exists)
{
return;
}
Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((titleId & 0x1000) != 0 ? "DLC" : "Title")} {titleId:X16}"); Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((titleId & 0x1000) != 0 ? "DLC" : "Title")} {titleId:X16}");
@@ -302,9 +307,16 @@ namespace Ryujinx.HLE.HOS
continue; continue;
} }
int oldCheatsCount = mods.Cheats.Count;
// A cheat file can contain several cheats for the same executable, so the file must be parsed in // A cheat file can contain several cheats for the same executable, so the file must be parsed in
// order to properly enumerate them. // order to properly enumerate them.
mods.Cheats.AddRange(GetCheatsInFile(file)); mods.Cheats.AddRange(GetCheatsInFile(file));
if (mods.Cheats.Count - oldCheatsCount > 0)
{
numMods++;
}
} }
return numMods; return numMods;
@@ -313,57 +325,54 @@ namespace Ryujinx.HLE.HOS
private static IEnumerable<Cheat> GetCheatsInFile(FileInfo cheatFile) private static IEnumerable<Cheat> GetCheatsInFile(FileInfo cheatFile)
{ {
string cheatName = DefaultCheatName; string cheatName = DefaultCheatName;
List<string> instructions = new List<string>(); List<string> instructions = new();
List<Cheat> cheats = new List<Cheat>(); List<Cheat> cheats = new();
using (StreamReader cheatData = cheatFile.OpenText()) using StreamReader cheatData = cheatFile.OpenText();
while (cheatData.ReadLine() is { } line)
{ {
string line; line = line.Trim();
while ((line = cheatData.ReadLine()) != null)
if (line.StartsWith('['))
{ {
line = line.Trim(); // This line starts a new cheat section.
if (!line.EndsWith(']') || line.Length < 3)
if (line.StartsWith('['))
{ {
// This line starts a new cheat section. // Skip the entire file if there's any error while parsing the cheat file.
if (!line.EndsWith(']') || line.Length < 3)
{
// Skip the entire file if there's any error while parsing the cheat file.
Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed"); Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed");
return new List<Cheat>(); return Array.Empty<Cheat>();
}
// Add the previous section to the list.
if (instructions.Count != 0)
{
cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
}
// Start a new cheat section.
cheatName = line.Substring(1, line.Length - 2);
instructions = new List<string>();
} }
else if (line.Length > 0)
// Add the previous section to the list.
if (instructions.Count > 0)
{ {
// The line contains an instruction. cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
instructions.Add(line);
} }
// Start a new cheat section.
cheatName = line.Substring(1, line.Length - 2);
instructions = new List<string>();
} }
else if (line.Length > 0)
// Add the last section being processed.
if (instructions.Count != 0)
{ {
cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions)); // The line contains an instruction.
instructions.Add(line);
} }
} }
// Add the last section being processed.
if (instructions.Count > 0)
{
cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
}
return cheats; return cheats;
} }
// Assumes searchDirPaths don't overlap // Assumes searchDirPaths don't overlap
public static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths) private static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths)
{ {
static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) || static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) ||
StrEquals(AmsNroPatchDir, name) || StrEquals(AmsNroPatchDir, name) ||
@@ -375,7 +384,7 @@ namespace Ryujinx.HLE.HOS
{ {
if (IsContentsDir(searchDir.Name)) if (IsContentsDir(searchDir.Name))
{ {
foreach (var (titleId, cache) in modCaches) foreach ((ulong titleId, ModCache cache) in modCaches)
{ {
QueryContentsDir(cache, searchDir, titleId); QueryContentsDir(cache, searchDir, titleId);
} }
@@ -419,15 +428,15 @@ namespace Ryujinx.HLE.HOS
foreach (ulong titleId in titles) foreach (ulong titleId in titles)
{ {
AppMods[titleId] = new ModCache(); _appMods[titleId] = new ModCache();
} }
CollectMods(AppMods, Patches, searchDirPaths); CollectMods(_appMods, _patches, searchDirPaths);
} }
internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage)
{ {
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0)
{ {
return baseStorage; return baseStorage;
} }
@@ -487,7 +496,7 @@ namespace Ryujinx.HLE.HOS
return newStorage; return newStorage;
} }
private static void AddFiles(IFileSystem fs, string modName, HashSet<string> fileSet, RomFsBuilder builder) private static void AddFiles(IFileSystem fs, string modName, ISet<string> fileSet, RomFsBuilder builder)
{ {
foreach (var entry in fs.EnumerateEntries() foreach (var entry in fs.EnumerateEntries()
.Where(f => f.Type == DirectoryEntryType.File) .Where(f => f.Type == DirectoryEntryType.File)
@@ -509,7 +518,7 @@ namespace Ryujinx.HLE.HOS
internal bool ReplaceExefsPartition(ulong titleId, ref IFileSystem exefs) internal bool ReplaceExefsPartition(ulong titleId, ref IFileSystem exefs)
{ {
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0)
{ {
return false; return false;
} }
@@ -537,13 +546,13 @@ namespace Ryujinx.HLE.HOS
internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos) internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos)
{ {
ModLoadResult modLoadResult = new ModLoadResult ModLoadResult modLoadResult = new()
{ {
Stubs = new BitVector32(), Stubs = new BitVector32(),
Replaces = new BitVector32() Replaces = new BitVector32()
}; };
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0)
{ {
return modLoadResult; return modLoadResult;
} }
@@ -561,7 +570,7 @@ namespace Ryujinx.HLE.HOS
{ {
var nsoName = ProcessConst.ExeFsPrefixes[i]; var nsoName = ProcessConst.ExeFsPrefixes[i];
FileInfo nsoFile = new FileInfo(Path.Combine(mod.Path.FullName, nsoName)); FileInfo nsoFile = new(Path.Combine(mod.Path.FullName, nsoName));
if (nsoFile.Exists) if (nsoFile.Exists)
{ {
if (modLoadResult.Replaces[1 << i]) if (modLoadResult.Replaces[1 << i])
@@ -580,7 +589,7 @@ namespace Ryujinx.HLE.HOS
modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension)); modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension));
} }
FileInfo npdmFile = new FileInfo(Path.Combine(mod.Path.FullName, "main.npdm")); FileInfo npdmFile = new(Path.Combine(mod.Path.FullName, "main.npdm"));
if (npdmFile.Exists) if (npdmFile.Exists)
{ {
if (modLoadResult.Npdm != null) if (modLoadResult.Npdm != null)
@@ -611,7 +620,7 @@ namespace Ryujinx.HLE.HOS
internal void ApplyNroPatches(NroExecutable nro) internal void ApplyNroPatches(NroExecutable nro)
{ {
var nroPatches = Patches.NroPatches; var nroPatches = _patches.NroPatches;
if (nroPatches.Count == 0) return; if (nroPatches.Count == 0) return;
@@ -622,9 +631,9 @@ namespace Ryujinx.HLE.HOS
internal bool ApplyNsoPatches(ulong titleId, params IExecutable[] programs) internal bool ApplyNsoPatches(ulong titleId, params IExecutable[] programs)
{ {
IEnumerable<Mod<DirectoryInfo>> nsoMods = Patches.NsoPatches; IEnumerable<Mod<DirectoryInfo>> nsoMods = _patches.NsoPatches;
if (AppMods.TryGetValue(titleId, out ModCache mods)) if (_appMods.TryGetValue(titleId, out ModCache mods))
{ {
nsoMods = nsoMods.Concat(mods.ExefsDirs); nsoMods = nsoMods.Concat(mods.ExefsDirs);
} }
@@ -636,7 +645,7 @@ namespace Ryujinx.HLE.HOS
internal void LoadCheats(ulong titleId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine) internal void LoadCheats(ulong titleId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine)
{ {
if (tamperInfo == null || tamperInfo.BuildIds == null || tamperInfo.CodeAddresses == null) if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null)
{ {
Logger.Error?.Print(LogClass.ModLoader, "Unable to install cheat because the associated process is invalid"); Logger.Error?.Print(LogClass.ModLoader, "Unable to install cheat because the associated process is invalid");
@@ -645,14 +654,14 @@ namespace Ryujinx.HLE.HOS
Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for title {titleId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}"); Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for title {titleId:X16}:\n {String.Join("\n ", tamperInfo.BuildIds)}");
if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0) if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0)
{ {
return; return;
} }
var cheats = mods.Cheats; var cheats = mods.Cheats;
var processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v }) var processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v })
.ToDictionary(x => x.k.Substring(0, Math.Min(Cheat.CheatIdSize, x.k.Length)), x => x.v); .ToDictionary(x => x.k[..Math.Min(Cheat.CheatIdSize, x.k.Length)], x => x.v);
foreach (var cheat in cheats) foreach (var cheat in cheats)
{ {
@@ -758,4 +767,4 @@ namespace Ryujinx.HLE.HOS
return count > 0; return count > 0;
} }
} }
} }

View File

@@ -2,6 +2,7 @@
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Loader; using LibHac.Loader;
using LibHac.Ns; using LibHac.Ns;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using ApplicationId = LibHac.Ncm.ApplicationId; using ApplicationId = LibHac.Ncm.ApplicationId;
@@ -17,8 +18,8 @@ namespace Ryujinx.HLE.Loaders.Processes
device.Configuration.VirtualFileSystem.ModLoader.CollectMods( device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
new[] { programId }, new[] { programId },
device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(), ModLoader.GetModsBasePath(),
device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath()); ModLoader.GetSdModsBasePath());
if (programId != 0) if (programId != 0)
{ {
@@ -36,4 +37,4 @@ namespace Ryujinx.HLE.Loaders.Processes
return processResult; return processResult;
} }
} }
} }

View File

@@ -8,6 +8,7 @@ using LibHac.Ns;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using ApplicationId = LibHac.Ncm.ApplicationId; using ApplicationId = LibHac.Ncm.ApplicationId;
@@ -35,8 +36,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
// Collecting mods related to AocTitleIds and ProgramId. // Collecting mods related to AocTitleIds and ProgramId.
device.Configuration.VirtualFileSystem.ModLoader.CollectMods( device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()), device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()),
device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(), ModLoader.GetModsBasePath(),
device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath()); ModLoader.GetSdModsBasePath());
// Load Nacp file. // Load Nacp file.
var nacpData = new BlitStruct<ApplicationControlProperty>(1); var nacpData = new BlitStruct<ApplicationControlProperty>(1);

View File

@@ -460,16 +460,16 @@ namespace Ryujinx.Ui.Widgets
private void OpenTitleModDir_Clicked(object sender, EventArgs args) private void OpenTitleModDir_Clicked(object sender, EventArgs args)
{ {
string modsBasePath = _virtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
{ {
string sdModsBasePath = _virtualFileSystem.ModLoader.GetSdModsBasePath(); string sdModsBasePath = ModLoader.GetSdModsBasePath();
string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }

View File

@@ -28,8 +28,8 @@ namespace Ryujinx.Ui.Windows
builder.Autoconnect(this); builder.Autoconnect(this);
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
_enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt"); _enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt");

View File

@@ -89,7 +89,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Adds an update to this list</property> <property name="tooltip_text" translatable="yes">Adds a DLC to this list</property>
<property name="margin_left">10</property> <property name="margin_left">10</property>
<signal name="clicked" handler="AddButton_Clicked" swapped="no"/> <signal name="clicked" handler="AddButton_Clicked" swapped="no"/>
</object> </object>
@@ -105,7 +105,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Removes the selected update</property> <property name="tooltip_text" translatable="yes">Removes the selected DLC</property>
<property name="margin_left">10</property> <property name="margin_left">10</property>
<signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/> <signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/>
</object> </object>
@@ -121,7 +121,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Removes the selected update</property> <property name="tooltip_text" translatable="yes">Removes all DLCs</property>
<property name="margin_left">10</property> <property name="margin_left">10</property>
<signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/> <signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/>
</object> </object>

View File

@@ -720,6 +720,7 @@ namespace Ryujinx.Ui.Windows
if (Directory.Exists(_addGameDirBox.Buffer.Text)) if (Directory.Exists(_addGameDirBox.Buffer.Text))
{ {
_gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text); _gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text);
_directoryChanged = true;
} }
else else
{ {
@@ -835,4 +836,4 @@ namespace Ryujinx.Ui.Windows
Dispose(); Dispose();
} }
} }
} }

View File

@@ -133,7 +133,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Removes the selected update</property> <property name="tooltip_text" translatable="yes">Removes all the updates</property>
<property name="margin_left">10</property> <property name="margin_left">10</property>
<signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/> <signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/>
</object> </object>