Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
952d013c67 | ||
|
46c8129bf5 | ||
|
8cfec5de4b | ||
|
37b6e081da | ||
|
3c3bcd82fe |
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Können Fehler verursachen",
|
"SettingsTabSystemHacksNote": " (Können Fehler verursachen)",
|
||||||
"SettingsTabSystemExpandDramSize": "Erweitere DRAM Größe auf 6GB",
|
"SettingsTabSystemExpandDramSize": "Erweitere DRAM Größe auf 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste",
|
"SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste",
|
||||||
"SettingsTabGraphics": "Grafik",
|
"SettingsTabGraphics": "Grafik",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Μικροδιορθώσεις",
|
"SettingsTabSystemHacks": "Μικροδιορθώσεις",
|
||||||
"SettingsTabSystemHacksNote": " - Μπορεί να προκαλέσουν αστάθεια",
|
"SettingsTabSystemHacksNote": " (Μπορεί να προκαλέσουν αστάθεια)",
|
||||||
"SettingsTabSystemExpandDramSize": "Επέκταση μεγέθους DRAM στα 6GB",
|
"SettingsTabSystemExpandDramSize": "Επέκταση μεγέθους DRAM στα 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν",
|
"SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν",
|
||||||
"SettingsTabGraphics": "Γραφικά",
|
"SettingsTabGraphics": "Γραφικά",
|
||||||
|
@@ -577,5 +577,7 @@
|
|||||||
"UserProfileNoImageError": "Profile image must be set",
|
"UserProfileNoImageError": "Profile image must be set",
|
||||||
"GameUpdateWindowHeading": "Updates Available for {0} [{1}]",
|
"GameUpdateWindowHeading": "Updates Available for {0} [{1}]",
|
||||||
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
|
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
|
||||||
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:"
|
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
|
||||||
|
"UserProfilesName": "Name:",
|
||||||
|
"UserProfilesUserId" : "User Id:"
|
||||||
}
|
}
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Pueden causar inestabilidad",
|
"SettingsTabSystemHacksNote": " (Pueden causar inestabilidad)",
|
||||||
"SettingsTabSystemExpandDramSize": "Expandir DRAM a 6GB",
|
"SettingsTabSystemExpandDramSize": "Expandir DRAM a 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados",
|
"SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados",
|
||||||
"SettingsTabGraphics": "Gráficos",
|
"SettingsTabGraphics": "Gráficos",
|
||||||
|
@@ -111,7 +111,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Cela peut causer des instabilitées",
|
"SettingsTabSystemHacksNote": " (Cela peut causer des instabilitées)",
|
||||||
"SettingsTabSystemExpandDramSize": "Augmenter la taille de la DRAM à 6GB",
|
"SettingsTabSystemExpandDramSize": "Augmenter la taille de la DRAM à 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquant",
|
"SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquant",
|
||||||
"SettingsTabGraphics": "Graphique",
|
"SettingsTabGraphics": "Graphique",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Possono causare instabilità",
|
"SettingsTabSystemHacksNote": " (Possono causare instabilità)",
|
||||||
"SettingsTabSystemExpandDramSize": "Espandi dimensione DRAM a 6GB",
|
"SettingsTabSystemExpandDramSize": "Espandi dimensione DRAM a 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti",
|
"SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti",
|
||||||
"SettingsTabGraphics": "Grafica",
|
"SettingsTabGraphics": "Grafica",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "해킹",
|
"SettingsTabSystemHacks": "해킹",
|
||||||
"SettingsTabSystemHacksNote": " - 불안정을 일으킬 수 있음",
|
"SettingsTabSystemHacksNote": " (불안정을 일으킬 수 있음)",
|
||||||
"SettingsTabSystemExpandDramSize": "DRAM 크기를 6GB로 확장",
|
"SettingsTabSystemExpandDramSize": "DRAM 크기를 6GB로 확장",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시",
|
"SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시",
|
||||||
"SettingsTabGraphics": "제도법",
|
"SettingsTabGraphics": "제도법",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": " - Pode causar instabilidade",
|
"SettingsTabSystemHacksNote": " (Pode causar instabilidade)",
|
||||||
"SettingsTabSystemExpandDramSize": "Expandir memória para 6GB",
|
"SettingsTabSystemExpandDramSize": "Expandir memória para 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados",
|
"SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados",
|
||||||
"SettingsTabGraphics": "Gráficos",
|
"SettingsTabGraphics": "Gráficos",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Хаки",
|
"SettingsTabSystemHacks": "Хаки",
|
||||||
"SettingsTabSystemHacksNote": " - Эти многие настройки вызывают нестабильность",
|
"SettingsTabSystemHacksNote": " (Эти многие настройки вызывают нестабильность)",
|
||||||
"SettingsTabSystemExpandDramSize": "Увеличение размера DRAM до 6GB",
|
"SettingsTabSystemExpandDramSize": "Увеличение размера DRAM до 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы",
|
"SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы",
|
||||||
"SettingsTabGraphics": "Графика",
|
"SettingsTabGraphics": "Графика",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "Hacklar",
|
"SettingsTabSystemHacks": "Hacklar",
|
||||||
"SettingsTabSystemHacksNote": " - Bunlar birçok dengesizlik oluşturabilir",
|
"SettingsTabSystemHacksNote": " (Bunlar birçok dengesizlik oluşturabilir)",
|
||||||
"SettingsTabSystemExpandDramSize": "DRAM boyutunu 6GB'a genişlet",
|
"SettingsTabSystemExpandDramSize": "DRAM boyutunu 6GB'a genişlet",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel",
|
"SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel",
|
||||||
"SettingsTabGraphics": "Grafikler",
|
"SettingsTabGraphics": "Grafikler",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "修正",
|
"SettingsTabSystemHacks": "修正",
|
||||||
"SettingsTabSystemHacksNote": " - 会引起模拟器不稳定",
|
"SettingsTabSystemHacksNote": " (会引起模拟器不稳定)",
|
||||||
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展到 6GB",
|
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展到 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
|
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
|
||||||
"SettingsTabGraphics": "图像",
|
"SettingsTabGraphics": "图像",
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
"SettingsTabSystemHacks": "修正",
|
"SettingsTabSystemHacks": "修正",
|
||||||
"SettingsTabSystemHacksNote": " - 會引起模擬器不穩定",
|
"SettingsTabSystemHacksNote": " (會引起模擬器不穩定)",
|
||||||
"SettingsTabSystemExpandDramSize": "將模擬記憶體大小擴充至 6GB",
|
"SettingsTabSystemExpandDramSize": "將模擬記憶體大小擴充至 6GB",
|
||||||
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服務",
|
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服務",
|
||||||
"SettingsTabGraphics": "圖形",
|
"SettingsTabGraphics": "圖形",
|
||||||
|
@@ -37,7 +37,7 @@
|
|||||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenDlcManager}"
|
Command="{Binding OpenDownloadableContentManager}"
|
||||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@@ -37,7 +37,7 @@
|
|||||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding OpenDlcManager}"
|
Command="{Binding OpenDownloadableContentManager}"
|
||||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@@ -17,9 +17,6 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
public UpdateWaitWindow()
|
public UpdateWaitWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,55 +1,87 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Controls.UserEditor"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d"
|
|
||||||
Padding="0"
|
|
||||||
Margin="0"
|
|
||||||
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
|
||||||
xmlns:models="clr-namespace:Ryujinx.Ava.Ui.Models"
|
xmlns:models="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||||
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
x:Class="Ryujinx.Ava.Ui.Controls.UserEditor">
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Grid Margin="0">
|
<Grid Margin="0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<StackPanel Orientation="Vertical" VerticalAlignment="Stretch" HorizontalAlignment="Left">
|
<StackPanel
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Orientation="Vertical">
|
||||||
<Image
|
<Image
|
||||||
|
Name="ProfileImage"
|
||||||
|
Width="96"
|
||||||
|
Height="96"
|
||||||
Margin="0"
|
Margin="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Height="96" Width="96"
|
|
||||||
Name="ProfileImage"
|
|
||||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||||
<Button Margin="5" Content="{Locale:Locale UserProfilesChangeProfileImage}"
|
<Button
|
||||||
Name="ChangePictureButton"
|
Name="ChangePictureButton"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
Click="ChangePictureButton_Click"
|
Click="ChangePictureButton_Click"
|
||||||
HorizontalAlignment="Stretch"/>
|
Content="{Locale:Locale UserProfilesChangeProfileImage}" />
|
||||||
<Button Margin="5" Content="{Locale:Locale UserProfilesSetProfileImage}"
|
<Button
|
||||||
Name="AddPictureButton"
|
Name="AddPictureButton"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
Click="ChangePictureButton_Click"
|
Click="ChangePictureButton_Click"
|
||||||
HorizontalAlignment="Stretch"/>
|
Content="{Locale:Locale UserProfilesSetProfileImage}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Row="0" Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="1" Spacing="10"
|
<StackPanel
|
||||||
Margin="5, 10">
|
Grid.Row="0"
|
||||||
<TextBox Name="NameBox" Width="300" Text="{Binding Name}" MaxLength="{Binding MaxProfileNameLength}"
|
Grid.Column="1"
|
||||||
HorizontalAlignment="Stretch" />
|
Margin="5,10"
|
||||||
<TextBlock Text="{Binding UserId}" Name="IdLabel" />
|
HorizontalAlignment="Stretch"
|
||||||
|
Orientation="Vertical"
|
||||||
|
Spacing="10">
|
||||||
|
<TextBlock Text="{Locale:Locale UserProfilesName}" />
|
||||||
|
<TextBox
|
||||||
|
Name="NameBox"
|
||||||
|
Width="300"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MaxLength="{Binding MaxProfileNameLength}"
|
||||||
|
Text="{Binding Name}" />
|
||||||
|
<TextBlock Text="{Locale:Locale UserProfilesUserId}" />
|
||||||
|
<TextBlock Name="IdLabel" Text="{Binding UserId}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
<StackPanel
|
||||||
<Button Content="{Locale:Locale Save}" Name="SaveButton" Click="SaveButton_Click"/>
|
Grid.Row="1"
|
||||||
<Button HorizontalAlignment="Right" Content="{Locale:Locale Discard}"
|
Grid.Column="0"
|
||||||
Name="CloseButton" Click="CloseButton_Click"/>
|
Grid.ColumnSpan="2"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="10">
|
||||||
|
<Button
|
||||||
|
Name="SaveButton"
|
||||||
|
Click="SaveButton_Click"
|
||||||
|
Content="{Locale:Locale Save}" />
|
||||||
|
<Button
|
||||||
|
Name="CloseButton"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Click="CloseButton_Click"
|
||||||
|
Content="{Locale:Locale Discard}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
@@ -63,7 +63,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
_parent?.GoBack();
|
_parent?.GoBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
private async void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
DataValidationErrors.ClearErrors(NameBox);
|
DataValidationErrors.ClearErrors(NameBox);
|
||||||
bool isInvalid = false;
|
bool isInvalid = false;
|
||||||
@@ -77,7 +77,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
|
|
||||||
if (TempProfile.Image == null)
|
if (TempProfile.Image == null)
|
||||||
{
|
{
|
||||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
|
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
|
||||||
|
|
||||||
isInvalid = true;
|
isInvalid = true;
|
||||||
}
|
}
|
||||||
|
@@ -1,28 +1,34 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Controls.UserSelector"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
|
||||||
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
|
||||||
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
|
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: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"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
|
||||||
x:Class="Ryujinx.Ava.Ui.Controls.UserSelector">
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<viewModels:UserProfileViewModel />
|
<viewModels:UserProfileViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<Grid HorizontalAlignment="Stretch"
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition />
|
<RowDefinition />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<ListBox HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5" Items="{Binding Profiles}"
|
<ListBox
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Center"
|
||||||
DoubleTapped="ProfilesList_DoubleTapped"
|
DoubleTapped="ProfilesList_DoubleTapped"
|
||||||
|
Items="{Binding Profiles}"
|
||||||
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
@@ -49,10 +55,11 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Image
|
<Image
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
|
Width="96"
|
||||||
|
Height="96"
|
||||||
Margin="0"
|
Margin="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Height="96" Width="96"
|
|
||||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@@ -68,23 +75,34 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<Border HorizontalAlignment="Left" VerticalAlignment="Top"
|
<Border
|
||||||
IsVisible="{Binding IsOpened}"
|
|
||||||
Background="LimeGreen"
|
|
||||||
Width="10"
|
Width="10"
|
||||||
Height="10"
|
Height="10"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
CornerRadius="5" />
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="LimeGreen"
|
||||||
|
CornerRadius="5"
|
||||||
|
IsVisible="{Binding IsOpened}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10,0" Spacing="10" HorizontalAlignment="Center">
|
<StackPanel
|
||||||
<Button Content="{Locale:Locale UserProfilesAddNewProfile}" Command="{Binding AddUser}" />
|
Grid.Row="1"
|
||||||
<Button IsEnabled="{Binding IsSelectedProfiledEditable}"
|
Margin="10,0"
|
||||||
Content="{Locale:Locale UserProfilesEditProfile}" Command="{Binding EditUser}" />
|
HorizontalAlignment="Center"
|
||||||
<Button IsEnabled="{Binding IsSelectedProfileDeletable}"
|
Orientation="Horizontal"
|
||||||
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}" Command="{Binding DeleteUser}" />
|
Spacing="10">
|
||||||
|
<Button Command="{Binding AddUser}" Content="{Locale:Locale UserProfilesAddNewProfile}" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding EditUser}"
|
||||||
|
Content="{Locale:Locale UserProfilesEditProfile}"
|
||||||
|
IsEnabled="{Binding IsSelectedProfiledEditable}" />
|
||||||
|
<Button
|
||||||
|
Command="{Binding DeleteUser}"
|
||||||
|
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}"
|
||||||
|
IsEnabled="{Binding IsSelectedProfileDeletable}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@@ -21,7 +21,7 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||||
{
|
{
|
||||||
NavigatedTo(e);
|
NavigatedTo(e);
|
||||||
}, Avalonia.Interactivity.RoutingStrategies.Direct);
|
}, RoutingStrategies.Direct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +29,10 @@ namespace Ryujinx.Ava.Ui.Controls
|
|||||||
{
|
{
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
switch (arg.NavigationMode)
|
if (arg.NavigationMode == NavigationMode.New)
|
||||||
{
|
{
|
||||||
case NavigationMode.New:
|
|
||||||
_parent = (NavigationDialogHost)arg.Parameter;
|
_parent = (NavigationDialogHost)arg.Parameter;
|
||||||
ViewModel = _parent.ViewModel;
|
ViewModel = _parent.ViewModel;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
@@ -22,7 +22,9 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
_isEnabled = value;
|
_isEnabled = value;
|
||||||
|
|
||||||
EnableToggled?.Invoke(this, _isEnabled);
|
EnableToggled?.Invoke(this, _isEnabled);
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,7 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
public string BuildId { get; }
|
public string BuildId { get; }
|
||||||
|
|
||||||
public string BuildIdKey => $"{BuildId}-{Name}";
|
public string BuildIdKey => $"{BuildId}-{Name}";
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string CleanName => Name.Substring(1, Name.Length - 8);
|
public string CleanName => Name.Substring(1, Name.Length - 8);
|
||||||
|
@@ -11,23 +11,10 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
{
|
{
|
||||||
BuildId = buildId;
|
BuildId = buildId;
|
||||||
Path = path;
|
Path = path;
|
||||||
|
|
||||||
CollectionChanged += CheatsList_CollectionChanged;
|
CollectionChanged += CheatsList_CollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheatsList_CollectionChanged(object sender,
|
|
||||||
NotifyCollectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Action == NotifyCollectionChangedAction.Add)
|
|
||||||
{
|
|
||||||
(e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Item_EnableToggled(object sender, bool e)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string BuildId { get; }
|
public string BuildId { get; }
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
@@ -47,5 +34,18 @@ namespace Ryujinx.Ava.Ui.Models
|
|||||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheatsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||||
|
{
|
||||||
|
(e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Item_EnableToggled(object sender, bool e)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,18 +0,0 @@
|
|||||||
namespace Ryujinx.Ava.Ui.Models
|
|
||||||
{
|
|
||||||
public class DlcModel
|
|
||||||
{
|
|
||||||
public bool IsEnabled { get; set; }
|
|
||||||
public string TitleId { get; }
|
|
||||||
public string ContainerPath { get; }
|
|
||||||
public string FullPath { get; }
|
|
||||||
|
|
||||||
public DlcModel(string titleId, string containerPath, string fullPath, bool isEnabled)
|
|
||||||
{
|
|
||||||
TitleId = titleId;
|
|
||||||
ContainerPath = containerPath;
|
|
||||||
FullPath = fullPath;
|
|
||||||
IsEnabled = isEnabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
18
Ryujinx.Ava/Ui/Models/DownloadableContentModel.cs
Normal file
18
Ryujinx.Ava/Ui/Models/DownloadableContentModel.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Ryujinx.Ava.Ui.Models
|
||||||
|
{
|
||||||
|
public class DownloadableContentModel
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public string TitleId { get; }
|
||||||
|
public string ContainerPath { get; }
|
||||||
|
public string FullPath { get; }
|
||||||
|
|
||||||
|
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||||
|
{
|
||||||
|
TitleId = titleId;
|
||||||
|
ContainerPath = containerPath;
|
||||||
|
FullPath = fullPath;
|
||||||
|
Enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -382,9 +382,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
{
|
{
|
||||||
string amiiboJsonString = await response.Content.ReadAsStringAsync();
|
string amiiboJsonString = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
|
using (FileStream amiiboJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
{
|
{
|
||||||
dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
|
amiiboJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
|
||||||
}
|
}
|
||||||
|
|
||||||
return amiiboJsonString;
|
return amiiboJsonString;
|
||||||
|
@@ -1261,15 +1261,15 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenDlcManager()
|
public async void OpenDownloadableContentManager()
|
||||||
{
|
{
|
||||||
var selection = SelectedApplication;
|
var selection = SelectedApplication;
|
||||||
|
|
||||||
if (selection != null)
|
if (selection != null)
|
||||||
{
|
{
|
||||||
DlcManagerWindow dlcManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
|
DownloadableContentManagerWindow downloadableContentManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
|
||||||
|
|
||||||
await dlcManager.ShowDialog(_owner);
|
await downloadableContentManager.ShowDialog(_owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -43,11 +43,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsHighlightedProfileEditable =>
|
public bool IsHighlightedProfileEditable => _highlightedProfile != null;
|
||||||
_highlightedProfile != null;
|
|
||||||
|
|
||||||
public bool IsHighlightedProfileDeletable =>
|
public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
||||||
_highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
|
||||||
|
|
||||||
public UserProfile HighlightedProfile
|
public UserProfile HighlightedProfile
|
||||||
{
|
{
|
||||||
@@ -62,16 +60,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadProfiles()
|
public void LoadProfiles()
|
||||||
{
|
{
|
||||||
Profiles.Clear();
|
Profiles.Clear();
|
||||||
|
|
||||||
var profiles = _owner.AccountManager.GetAllUsers()
|
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||||
.OrderByDescending(x => x.AccountState == AccountState.Open);
|
|
||||||
|
|
||||||
foreach (var profile in profiles)
|
foreach (var profile in profiles)
|
||||||
{
|
{
|
||||||
@@ -94,6 +89,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
|
|||||||
public void AddUser()
|
public void AddUser()
|
||||||
{
|
{
|
||||||
UserProfile userProfile = null;
|
UserProfile userProfile = null;
|
||||||
|
|
||||||
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
@@ -27,9 +26,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_ = DownloadPatronsJson();
|
_ = DownloadPatronsJson();
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
@@ -18,9 +17,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +28,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||||
|
@@ -1,21 +1,24 @@
|
|||||||
<window:StyleableWindow x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
|
<window:StyleableWindow
|
||||||
|
x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
|
||||||
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:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
|
xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
|
||||||
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
|
||||||
mc:Ignorable="d"
|
Width="500"
|
||||||
Width="500" MinHeight="500" Height="500"
|
Height="500"
|
||||||
|
MinWidth="500"
|
||||||
|
MinHeight="500"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
MinWidth="500">
|
mc:Ignorable="d">
|
||||||
<Window.Styles>
|
<Window.Styles>
|
||||||
<Style Selector="TreeViewItem">
|
<Style Selector="TreeViewItem">
|
||||||
<Setter Property="IsExpanded" Value="True" />
|
<Setter Property="IsExpanded" Value="True" />
|
||||||
</Style>
|
</Style>
|
||||||
</Window.Styles>
|
</Window.Styles>
|
||||||
<Grid Name="DlcGrid" Margin="15">
|
<Grid Name="CheatGrid" Margin="15">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -24,14 +27,14 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
MaxWidth="500"
|
||||||
Margin="20,15,20,20"
|
Margin="20,15,20,20"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
MaxWidth="500"
|
|
||||||
LineHeight="18"
|
LineHeight="18"
|
||||||
TextWrapping="Wrap"
|
|
||||||
Text="{Binding Heading}"
|
Text="{Binding Heading}"
|
||||||
TextAlignment="Center" />
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
<Border
|
<Border
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
@@ -39,32 +42,38 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
BorderBrush="Gray"
|
BorderBrush="Gray"
|
||||||
BorderThickness="1">
|
BorderThickness="1">
|
||||||
<TreeView Items="{Binding LoadedCheats}"
|
<TreeView
|
||||||
|
Name="CheatsView"
|
||||||
|
MinHeight="300"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
Name="CheatsView"
|
Items="{Binding LoadedCheats}">
|
||||||
MinHeight="300">
|
|
||||||
<TreeView.Styles>
|
<TreeView.Styles>
|
||||||
<Styles>
|
<Styles>
|
||||||
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
|
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
|
||||||
<Setter Property="IsVisible" Value="False"/>
|
<Setter Property="IsVisible" Value="False" />
|
||||||
</Style>
|
</Style>
|
||||||
</Styles>
|
</Styles>
|
||||||
</TreeView.Styles>
|
</TreeView.Styles>
|
||||||
<TreeView.DataTemplates>
|
<TreeView.DataTemplates>
|
||||||
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
|
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
|
||||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||||
<CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
|
<CheckBox MinWidth="20" IsChecked="{Binding IsEnabled}" />
|
||||||
<TextBlock Width="150"
|
<TextBlock Width="150" Text="{Binding BuildId}" />
|
||||||
Text="{Binding BuildId}" />
|
<TextBlock Text="{Binding Path}" />
|
||||||
<TextBlock
|
|
||||||
Text="{Binding Path}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
<DataTemplate x:DataType="model:CheatModel">
|
<DataTemplate x:DataType="model:CheatModel">
|
||||||
<StackPanel Orientation="Horizontal" Margin="0" HorizontalAlignment="Left">
|
<StackPanel
|
||||||
<CheckBox IsChecked="{Binding IsEnabled}" Padding="0" Margin="5,0" MinWidth="20" />
|
Margin="0"
|
||||||
<TextBlock Text="{Binding CleanName}" VerticalAlignment="Center" />
|
HorizontalAlignment="Left"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<CheckBox
|
||||||
|
MinWidth="20"
|
||||||
|
Margin="5,0"
|
||||||
|
Padding="0"
|
||||||
|
IsChecked="{Binding IsEnabled}" />
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding CleanName}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</TreeView.DataTemplates>
|
</TreeView.DataTemplates>
|
||||||
@@ -79,8 +88,8 @@
|
|||||||
Name="SaveButton"
|
Name="SaveButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
IsVisible="{Binding !NoCheatsFound}"
|
Command="{Binding Save}"
|
||||||
Command="{Binding Save}">
|
IsVisible="{Binding !NoCheatsFound}">
|
||||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
@@ -26,7 +25,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||||
}
|
}
|
||||||
@@ -38,9 +36,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
|
Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
|
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
|
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
|
||||||
@@ -96,12 +91,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
if (NoCheatsFound)
|
if (NoCheatsFound)
|
||||||
|
@@ -3,24 +3,14 @@ using Avalonia.Controls.Primitives;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.LogicalTree;
|
using Avalonia.LogicalTree;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using Ryujinx.Input.Assigner;
|
using Ryujinx.Input.Assigner;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Key = Ryujinx.Input.Key;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
|
@@ -1,254 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Collections;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using LibHac.Common;
|
|
||||||
using LibHac.Fs;
|
|
||||||
using LibHac.Fs.Fsa;
|
|
||||||
using LibHac.FsSystem;
|
|
||||||
using LibHac.Tools.Fs;
|
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
|
||||||
using Ryujinx.Ava.Ui.Models;
|
|
||||||
using Ryujinx.Common.Configuration;
|
|
||||||
using Ryujinx.Common.Utilities;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Path = System.IO.Path;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
|
||||||
{
|
|
||||||
public partial class DlcManagerWindow : StyleableWindow
|
|
||||||
{
|
|
||||||
private readonly List<DlcContainer> _dlcContainerList;
|
|
||||||
private readonly string _dlcJsonPath;
|
|
||||||
|
|
||||||
public VirtualFileSystem VirtualFileSystem { get; }
|
|
||||||
|
|
||||||
public AvaloniaList<DlcModel> Dlcs { get; set; }
|
|
||||||
public ulong TitleId { get; }
|
|
||||||
public string TitleName { get; }
|
|
||||||
|
|
||||||
public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
|
|
||||||
|
|
||||||
public DlcManagerWindow()
|
|
||||||
{
|
|
||||||
DataContext = this;
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
|
||||||
}
|
|
||||||
|
|
||||||
public DlcManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
|
||||||
{
|
|
||||||
VirtualFileSystem = virtualFileSystem;
|
|
||||||
TitleId = titleId;
|
|
||||||
TitleName = titleName;
|
|
||||||
|
|
||||||
_dlcJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_dlcContainerList = new List<DlcContainer>();
|
|
||||||
}
|
|
||||||
|
|
||||||
DataContext = this;
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
|
||||||
|
|
||||||
LoadDlcs();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadDlcs()
|
|
||||||
{
|
|
||||||
foreach (DlcContainer dlcContainer in _dlcContainerList)
|
|
||||||
{
|
|
||||||
using FileStream containerFile = File.OpenRead(dlcContainer.Path);
|
|
||||||
|
|
||||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
|
||||||
|
|
||||||
VirtualFileSystem.ImportTickets(pfs);
|
|
||||||
|
|
||||||
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
|
|
||||||
{
|
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
|
|
||||||
|
|
||||||
if (nca != null)
|
|
||||||
{
|
|
||||||
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), dlcContainer.Path, dlcNca.Path,
|
|
||||||
dlcNca.Enabled));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new Nca(VirtualFileSystem.KeySet, ncaStorage);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[
|
|
||||||
"DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddDlc(string path)
|
|
||||||
{
|
|
||||||
if (!File.Exists(path) || Dlcs.FirstOrDefault(x => x.ContainerPath == path) != null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (FileStream containerFile = File.OpenRead(path))
|
|
||||||
{
|
|
||||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
|
||||||
bool containsDlc = false;
|
|
||||||
|
|
||||||
VirtualFileSystem.ImportTickets(pfs);
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
|
||||||
|
|
||||||
if (nca == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
|
||||||
{
|
|
||||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
|
||||||
|
|
||||||
containsDlc = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!containsDlc)
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveDlcs(bool removeSelectedOnly = false)
|
|
||||||
{
|
|
||||||
if (removeSelectedOnly)
|
|
||||||
{
|
|
||||||
Dlcs.RemoveAll(Dlcs.Where(x => x.IsEnabled).ToList());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Dlcs.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveSelected()
|
|
||||||
{
|
|
||||||
RemoveDlcs(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveAll()
|
|
||||||
{
|
|
||||||
RemoveDlcs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Add()
|
|
||||||
{
|
|
||||||
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectDlcDialogTitle"], AllowMultiple = true };
|
|
||||||
|
|
||||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
|
||||||
|
|
||||||
string[] files = await dialog.ShowAsync(this);
|
|
||||||
|
|
||||||
if (files != null)
|
|
||||||
{
|
|
||||||
foreach (string file in files)
|
|
||||||
{
|
|
||||||
await AddDlc(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
|
||||||
{
|
|
||||||
_dlcContainerList.Clear();
|
|
||||||
|
|
||||||
DlcContainer container = default;
|
|
||||||
|
|
||||||
foreach (DlcModel dlc in Dlcs)
|
|
||||||
{
|
|
||||||
if (container.Path != dlc.ContainerPath)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(container.Path))
|
|
||||||
{
|
|
||||||
_dlcContainerList.Add(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
container = new DlcContainer { Path = dlc.ContainerPath, DlcNcaList = new List<DlcNca>() };
|
|
||||||
}
|
|
||||||
|
|
||||||
container.DlcNcaList.Add(new DlcNca
|
|
||||||
{
|
|
||||||
Enabled = dlc.IsEnabled,
|
|
||||||
TitleId = Convert.ToUInt64(dlc.TitleId, 16),
|
|
||||||
Path = dlc.FullPath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(container.Path))
|
|
||||||
{
|
|
||||||
_dlcContainerList.Add(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
|
|
||||||
{
|
|
||||||
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,5 @@
|
|||||||
<window:StyleableWindow
|
<window:StyleableWindow
|
||||||
x:Class="Ryujinx.Ava.Ui.Windows.DlcManagerWindow"
|
x:Class="Ryujinx.Ava.Ui.Windows.DownloadableContentManagerWindow"
|
||||||
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:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
MinWidth="600"
|
MinWidth="600"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<Grid Name="DlcGrid" Margin="15">
|
<Grid Name="DownloadableContentGrid" Margin="15">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalScrollBarVisibility="Auto"
|
HorizontalScrollBarVisibility="Auto"
|
||||||
Items="{Binding Dlcs}"
|
Items="{Binding DownloadableContents}"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTemplateColumn Width="90">
|
<DataGridTemplateColumn Width="90">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
Width="50"
|
Width="50"
|
||||||
MinWidth="40"
|
MinWidth="40"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
IsChecked="{Binding IsEnabled}" />
|
IsChecked="{Binding Enabled}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
<DataGridTemplateColumn.Header>
|
<DataGridTemplateColumn.Header>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
Name="SaveButton"
|
Name="SaveButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding Save}">
|
Command="{Binding SaveAndClose}">
|
||||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
266
Ryujinx.Ava/Ui/Windows/DownloadableContentManagerWindow.axaml.cs
Normal file
266
Ryujinx.Ava/Ui/Windows/DownloadableContentManagerWindow.axaml.cs
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
|
{
|
||||||
|
public partial class DownloadableContentManagerWindow : StyleableWindow
|
||||||
|
{
|
||||||
|
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||||
|
private readonly string _downloadableContentJsonPath;
|
||||||
|
|
||||||
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
|
public AvaloniaList<DownloadableContentModel> DownloadableContents { get; set; } = new AvaloniaList<DownloadableContentModel>();
|
||||||
|
public ulong TitleId { get; }
|
||||||
|
public string TitleName { get; }
|
||||||
|
|
||||||
|
public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
|
||||||
|
|
||||||
|
public DownloadableContentManagerWindow()
|
||||||
|
{
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||||
|
{
|
||||||
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
TitleId = titleId;
|
||||||
|
TitleName = titleName;
|
||||||
|
|
||||||
|
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
|
||||||
|
|
||||||
|
LoadDownloadableContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadDownloadableContents()
|
||||||
|
{
|
||||||
|
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||||
|
{
|
||||||
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
|
{
|
||||||
|
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||||
|
|
||||||
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
|
|
||||||
|
VirtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref(), downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||||
|
if (nca != null)
|
||||||
|
{
|
||||||
|
DownloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||||
|
downloadableContentContainer.ContainerPath,
|
||||||
|
downloadableContentNca.FullPath,
|
||||||
|
downloadableContentNca.Enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Save the list again to remove leftovers.
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new Nca(VirtualFileSystem.KeySet, ncaStorage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddDownloadableContent(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (FileStream containerFile = File.OpenRead(path))
|
||||||
|
{
|
||||||
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
|
bool containsDownloadableContent = false;
|
||||||
|
|
||||||
|
VirtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
||||||
|
|
||||||
|
if (nca == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||||
|
{
|
||||||
|
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
||||||
|
|
||||||
|
containsDownloadableContent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containsDownloadableContent)
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDownloadableContents(bool removeSelectedOnly = false)
|
||||||
|
{
|
||||||
|
if (removeSelectedOnly)
|
||||||
|
{
|
||||||
|
DownloadableContents.RemoveAll(DownloadableContents.Where(x => x.Enabled).ToList());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DownloadableContents.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSelected()
|
||||||
|
{
|
||||||
|
RemoveDownloadableContents(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAll()
|
||||||
|
{
|
||||||
|
RemoveDownloadableContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Add()
|
||||||
|
{
|
||||||
|
OpenFileDialog dialog = new OpenFileDialog()
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance["SelectDlcDialogTitle"],
|
||||||
|
AllowMultiple = true
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.Filters.Add(new FileDialogFilter
|
||||||
|
{
|
||||||
|
Name = "NSP",
|
||||||
|
Extensions = { "nsp" }
|
||||||
|
});
|
||||||
|
|
||||||
|
string[] files = await dialog.ShowAsync(this);
|
||||||
|
|
||||||
|
if (files != null)
|
||||||
|
{
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
await AddDownloadableContent(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList.Clear();
|
||||||
|
|
||||||
|
DownloadableContentContainer container = default;
|
||||||
|
|
||||||
|
foreach (DownloadableContentModel downloadableContent in DownloadableContents)
|
||||||
|
{
|
||||||
|
if (container.ContainerPath != downloadableContent.ContainerPath)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
container = new DownloadableContentContainer
|
||||||
|
{
|
||||||
|
ContainerPath = downloadableContent.ContainerPath,
|
||||||
|
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
container.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||||
|
{
|
||||||
|
Enabled = downloadableContent.Enabled,
|
||||||
|
TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
|
||||||
|
FullPath = downloadableContent.FullPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
|
{
|
||||||
|
downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveAndClose()
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -257,7 +257,7 @@
|
|||||||
</DockPanel>
|
</DockPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ContentControl
|
<ContentControl
|
||||||
Name="Content"
|
Name="MainContent"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
|
@@ -2,10 +2,8 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.Win32;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
@@ -33,7 +31,7 @@ using System.IO;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||||
using ProgressBar = Avalonia.Controls.ProgressBar;
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
public partial class MainWindow : StyleableWindow
|
public partial class MainWindow : StyleableWindow
|
||||||
@@ -87,7 +85,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Load();
|
Load();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
UiHandler = new AvaHostUiHandler(this);
|
UiHandler = new AvaHostUiHandler(this);
|
||||||
|
|
||||||
@@ -110,12 +107,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
_rendererWaitEvent = new AutoResetEvent(false);
|
_rendererWaitEvent = new AutoResetEvent(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadGameList()
|
public void LoadGameList()
|
||||||
{
|
{
|
||||||
if (_isLoading)
|
if (_isLoading)
|
||||||
@@ -244,7 +235,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
PrepareLoadScreen();
|
PrepareLoadScreen();
|
||||||
|
|
||||||
_mainViewContent = Content.Content as Control;
|
_mainViewContent = MainContent.Content as Control;
|
||||||
|
|
||||||
GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
||||||
AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
||||||
@@ -311,7 +302,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
Content.Content = GlRenderer;
|
MainContent.Content = GlRenderer;
|
||||||
|
|
||||||
if (startFullscreen && WindowState != WindowState.FullScreen)
|
if (startFullscreen && WindowState != WindowState.FullScreen)
|
||||||
{
|
{
|
||||||
@@ -355,9 +346,9 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
if (Content.Content != _mainViewContent)
|
if (MainContent.Content != _mainViewContent)
|
||||||
{
|
{
|
||||||
Content.Content = _mainViewContent;
|
MainContent.Content = _mainViewContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewModel.ShowMenuAndStatusBar = true;
|
ViewModel.ShowMenuAndStatusBar = true;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
|
@@ -1,19 +1,14 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Presenters;
|
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.LogicalTree;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using FluentAvalonia.Core;
|
using FluentAvalonia.Core;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
|
||||||
using Ryujinx.Ava.Ui.ViewModels;
|
using Ryujinx.Ava.Ui.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
@@ -23,8 +18,6 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
|
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
@@ -44,7 +37,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Load();
|
Load();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()));
|
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()));
|
||||||
MultiBinding tzMultiBinding = new() { Converter = converter };
|
MultiBinding tzMultiBinding = new() { Converter = converter };
|
||||||
@@ -62,13 +54,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Load();
|
Load();
|
||||||
AttachDebugDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Load()
|
private void Load()
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using FluentAvalonia.UI.Controls;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Threading;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Ui.Controls;
|
using Ryujinx.Ava.Ui.Controls;
|
||||||
using Ryujinx.Ava.Ui.Models;
|
using Ryujinx.Ava.Ui.Models;
|
||||||
@@ -23,14 +24,12 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Ui.Windows
|
namespace Ryujinx.Ava.Ui.Windows
|
||||||
{
|
{
|
||||||
public partial class TitleUpdateWindow : StyleableWindow
|
public partial class TitleUpdateWindow : StyleableWindow
|
||||||
{
|
{
|
||||||
private readonly string _updateJsonPath;
|
private readonly string _titleUpdateJsonPath;
|
||||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||||
|
|
||||||
public VirtualFileSystem VirtualFileSystem { get; }
|
public VirtualFileSystem VirtualFileSystem { get; }
|
||||||
@@ -46,7 +45,6 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||||
}
|
}
|
||||||
@@ -57,33 +55,30 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
TitleName = titleName;
|
TitleName = titleName;
|
||||||
|
|
||||||
_updateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
|
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_titleUpdateWindowData = new TitleUpdateMetadata {Selected = "", Paths = new List<string>()};
|
_titleUpdateWindowData = new TitleUpdateMetadata
|
||||||
|
{
|
||||||
|
Selected = "",
|
||||||
|
Paths = new List<string>()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AttachDebugDevTools();
|
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
|
||||||
|
|
||||||
LoadUpdates();
|
LoadUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private void AttachDebugDevTools()
|
|
||||||
{
|
|
||||||
this.AttachDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadUpdates()
|
private void LoadUpdates()
|
||||||
{
|
{
|
||||||
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||||
@@ -126,8 +121,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(Nca patchNca, Nca controlNca) =
|
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
||||||
ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
|
|
||||||
|
|
||||||
if (controlNca != null && patchNca != null)
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
@@ -135,11 +129,8 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
using var nacpFile = new UniqueRef<IFile>();
|
using var nacpFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
|
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
.OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read)
|
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||||
.ThrowIfFailure();
|
|
||||||
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
|
|
||||||
.ThrowIfFailure();
|
|
||||||
|
|
||||||
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
|
||||||
}
|
}
|
||||||
@@ -190,9 +181,17 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
|
|
||||||
public async void Add()
|
public async void Add()
|
||||||
{
|
{
|
||||||
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectUpdateDialogTitle"], AllowMultiple = true };
|
OpenFileDialog dialog = new OpenFileDialog()
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance["SelectUpdateDialogTitle"],
|
||||||
|
AllowMultiple = true
|
||||||
|
};
|
||||||
|
|
||||||
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
|
dialog.Filters.Add(new FileDialogFilter
|
||||||
|
{
|
||||||
|
Name = "NSP",
|
||||||
|
Extensions = { "nsp" }
|
||||||
|
});
|
||||||
|
|
||||||
string[] files = await dialog.ShowAsync(this);
|
string[] files = await dialog.ShowAsync(this);
|
||||||
|
|
||||||
@@ -222,12 +221,10 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Version.Parse(first.Control.DisplayVersionString.ToString())
|
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||||
.CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
TitleUpdates.Clear();
|
TitleUpdates.Clear();
|
||||||
|
|
||||||
TitleUpdates.AddRange(list);
|
TitleUpdates.AddRange(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,9 +244,9 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
|
using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
|
||||||
{
|
{
|
||||||
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Owner is MainWindow window)
|
if (Owner is MainWindow window)
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
using System;
|
using System;
|
||||||
@@ -23,9 +22,7 @@ namespace Ryujinx.Ava.Ui.Windows
|
|||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
Title = LocaleManager.Instance["RyujinxUpdater"];
|
Title = LocaleManager.Instance["RyujinxUpdater"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Ryujinx.Common.Configuration
|
|
||||||
{
|
|
||||||
public struct DlcContainer
|
|
||||||
{
|
|
||||||
public string Path { get; set; }
|
|
||||||
public List<DlcNca> DlcNcaList { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
namespace Ryujinx.Common.Configuration
|
|
||||||
{
|
|
||||||
public struct DlcNca
|
|
||||||
{
|
|
||||||
public string Path { get; set; }
|
|
||||||
public ulong TitleId { get; set; }
|
|
||||||
public bool Enabled { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
13
Ryujinx.Common/Configuration/DownloadableContentContainer.cs
Normal file
13
Ryujinx.Common/Configuration/DownloadableContentContainer.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Configuration
|
||||||
|
{
|
||||||
|
public struct DownloadableContentContainer
|
||||||
|
{
|
||||||
|
[JsonPropertyName("path")]
|
||||||
|
public string ContainerPath { get; set; }
|
||||||
|
[JsonPropertyName("dlc_nca_list")]
|
||||||
|
public List<DownloadableContentNca> DownloadableContentNcaList { get; set; }
|
||||||
|
}
|
||||||
|
}
|
14
Ryujinx.Common/Configuration/DownloadableContentNca.cs
Normal file
14
Ryujinx.Common/Configuration/DownloadableContentNca.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Configuration
|
||||||
|
{
|
||||||
|
public struct DownloadableContentNca
|
||||||
|
{
|
||||||
|
[JsonPropertyName("path")]
|
||||||
|
public string FullPath { get; set; }
|
||||||
|
[JsonPropertyName("title_id")]
|
||||||
|
public ulong TitleId { get; set; }
|
||||||
|
[JsonPropertyName("is_enabled")]
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -216,13 +216,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
|
|||||||
{
|
{
|
||||||
var target = memoryManager.Physical.TextureCache.FindTexture(
|
var target = memoryManager.Physical.TextureCache.FindTexture(
|
||||||
memoryManager,
|
memoryManager,
|
||||||
dst,
|
|
||||||
dstGpuVa,
|
dstGpuVa,
|
||||||
dstBpp,
|
dstBpp,
|
||||||
dstStride,
|
dstStride,
|
||||||
|
dst.Height,
|
||||||
xCount,
|
xCount,
|
||||||
yCount,
|
yCount,
|
||||||
dstLinear);
|
dstLinear,
|
||||||
|
dst.MemoryLayout);
|
||||||
|
|
||||||
if (target != null)
|
if (target != null)
|
||||||
{
|
{
|
||||||
|
@@ -59,9 +59,24 @@ namespace Ryujinx.Graphics.Gpu
|
|||||||
{
|
{
|
||||||
oldMemoryManager.Physical.BufferCache.NotifyBuffersModified -= BufferManager.Rebind;
|
oldMemoryManager.Physical.BufferCache.NotifyBuffersModified -= BufferManager.Rebind;
|
||||||
oldMemoryManager.Physical.DecrementReferenceCount();
|
oldMemoryManager.Physical.DecrementReferenceCount();
|
||||||
|
oldMemoryManager.MemoryUnmapped -= MemoryUnmappedHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryManager.Physical.BufferCache.NotifyBuffersModified += BufferManager.Rebind;
|
memoryManager.Physical.BufferCache.NotifyBuffersModified += BufferManager.Rebind;
|
||||||
|
memoryManager.MemoryUnmapped += MemoryUnmappedHandler;
|
||||||
|
|
||||||
|
// Since the memory manager changed, make sure we will get pools from addresses of the new memory manager.
|
||||||
|
TextureManager.ReloadPools();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory mappings change event handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">Memory manager where the mappings changed</param>
|
||||||
|
/// <param name="e">Information about the region that is being changed</param>
|
||||||
|
private void MemoryUnmappedHandler(object sender, UnmapEventArgs e)
|
||||||
|
{
|
||||||
|
TextureManager.ReloadPools();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
129
Ryujinx.Graphics.Gpu/Image/PoolCache.cs
Normal file
129
Ryujinx.Graphics.Gpu/Image/PoolCache.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resource pool interface.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Resource pool type</typeparam>
|
||||||
|
interface IPool<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Start address of the pool in memory.
|
||||||
|
/// </summary>
|
||||||
|
ulong Address { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Linked list node used on the texture pool cache.
|
||||||
|
/// </summary>
|
||||||
|
LinkedListNode<T> CacheNode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp set on the last use of the pool by the cache.
|
||||||
|
/// </summary>
|
||||||
|
ulong CacheTimestamp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pool cache.
|
||||||
|
/// This can keep multiple pools, and return the current one as needed.
|
||||||
|
/// </summary>
|
||||||
|
abstract class PoolCache<T> : IDisposable where T : IPool<T>, IDisposable
|
||||||
|
{
|
||||||
|
private const int MaxCapacity = 2;
|
||||||
|
private const ulong MinDeltaForRemoval = 20000;
|
||||||
|
|
||||||
|
private readonly GpuContext _context;
|
||||||
|
private readonly LinkedList<T> _pools;
|
||||||
|
private ulong _currentTimestamp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
|
public PoolCache(GpuContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_pools = new LinkedList<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increments the internal timestamp of the cache that is used to decide when old resources will be deleted.
|
||||||
|
/// </summary>
|
||||||
|
public void Tick()
|
||||||
|
{
|
||||||
|
_currentTimestamp++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a cache texture pool, or creates a new one if not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channel">GPU channel that the texture pool cache belongs to</param>
|
||||||
|
/// <param name="address">Start address of the texture pool</param>
|
||||||
|
/// <param name="maximumId">Maximum ID of the texture pool</param>
|
||||||
|
/// <returns>The found or newly created texture pool</returns>
|
||||||
|
public T FindOrCreate(GpuChannel channel, ulong address, int maximumId)
|
||||||
|
{
|
||||||
|
// Remove old entries from the cache, if possible.
|
||||||
|
while (_pools.Count > MaxCapacity && (_currentTimestamp - _pools.First.Value.CacheTimestamp) >= MinDeltaForRemoval)
|
||||||
|
{
|
||||||
|
T oldestPool = _pools.First.Value;
|
||||||
|
|
||||||
|
_pools.RemoveFirst();
|
||||||
|
oldestPool.Dispose();
|
||||||
|
oldestPool.CacheNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
T pool;
|
||||||
|
|
||||||
|
// Try to find the pool on the cache.
|
||||||
|
for (LinkedListNode<T> node = _pools.First; node != null; node = node.Next)
|
||||||
|
{
|
||||||
|
pool = node.Value;
|
||||||
|
|
||||||
|
if (pool.Address == address)
|
||||||
|
{
|
||||||
|
if (pool.CacheNode != _pools.Last)
|
||||||
|
{
|
||||||
|
_pools.Remove(pool.CacheNode);
|
||||||
|
|
||||||
|
pool.CacheNode = _pools.AddLast(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.CacheTimestamp = _currentTimestamp;
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, create a new one.
|
||||||
|
pool = CreatePool(_context, channel, address, maximumId);
|
||||||
|
|
||||||
|
pool.CacheNode = _pools.AddLast(pool);
|
||||||
|
pool.CacheTimestamp = _currentTimestamp;
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the pool belongs to</param>
|
||||||
|
/// <param name="channel">GPU channel that the pool belongs to</param>
|
||||||
|
/// <param name="address">Address of the pool in guest memory</param>
|
||||||
|
/// <param name="maximumId">Maximum ID of the pool (equal to maximum minus one)</param>
|
||||||
|
protected abstract T CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (T pool in _pools)
|
||||||
|
{
|
||||||
|
pool.Dispose();
|
||||||
|
pool.CacheNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pools.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,16 +1,27 @@
|
|||||||
using Ryujinx.Graphics.Gpu.Memory;
|
using Ryujinx.Graphics.Gpu.Memory;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sampler pool.
|
/// Sampler pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class SamplerPool : Pool<Sampler, SamplerDescriptor>
|
class SamplerPool : Pool<Sampler, SamplerDescriptor>, IPool<SamplerPool>
|
||||||
{
|
{
|
||||||
private float _forcedAnisotropy;
|
private float _forcedAnisotropy;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new instance of the sampler pool.
|
/// Linked list node used on the sampler pool cache.
|
||||||
|
/// </summary>
|
||||||
|
public LinkedListNode<SamplerPool> CacheNode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp used by the sampler pool cache, updated on every use of this sampler pool.
|
||||||
|
/// </summary>
|
||||||
|
public ulong CacheTimestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the sampler pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">GPU context that the sampler pool belongs to</param>
|
/// <param name="context">GPU context that the sampler pool belongs to</param>
|
||||||
/// <param name="physicalMemory">Physical memory where the sampler descriptors are mapped</param>
|
/// <param name="physicalMemory">Physical memory where the sampler descriptors are mapped</param>
|
||||||
|
30
Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs
Normal file
30
Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sampler pool cache.
|
||||||
|
/// This can keep multiple sampler pools, and return the current one as needed.
|
||||||
|
/// It is useful for applications that uses multiple sampler pools.
|
||||||
|
/// </summary>
|
||||||
|
class SamplerPoolCache : PoolCache<SamplerPool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new instance of the texture pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
|
public SamplerPoolCache(GpuContext context) : base(context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the sampler pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">GPU context that the sampler pool belongs to</param>
|
||||||
|
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
||||||
|
/// <param name="address">Address of the sampler pool in guest memory</param>
|
||||||
|
/// <param name="maximumId">Maximum sampler ID of the sampler pool (equal to maximum samplers minus one)</param>
|
||||||
|
protected override SamplerPool CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId)
|
||||||
|
{
|
||||||
|
return new SamplerPool(context, channel.MemoryManager.Physical, address, maximumId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Texture bindings manager.
|
/// Texture bindings manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TextureBindingsManager : IDisposable
|
class TextureBindingsManager
|
||||||
{
|
{
|
||||||
private const int InitialTextureStateSize = 32;
|
private const int InitialTextureStateSize = 32;
|
||||||
private const int InitialImageStateSize = 8;
|
private const int InitialImageStateSize = 8;
|
||||||
@@ -22,15 +22,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
private readonly bool _isCompute;
|
private readonly bool _isCompute;
|
||||||
|
|
||||||
private SamplerPool _samplerPool;
|
private ulong _texturePoolGpuVa;
|
||||||
|
|
||||||
private SamplerIndex _samplerIndex;
|
|
||||||
|
|
||||||
private ulong _texturePoolAddress;
|
|
||||||
private int _texturePoolMaximumId;
|
private int _texturePoolMaximumId;
|
||||||
|
private TexturePool _texturePool;
|
||||||
|
private ulong _samplerPoolGpuVa;
|
||||||
|
private int _samplerPoolMaximumId;
|
||||||
|
private SamplerIndex _samplerIndex;
|
||||||
|
private SamplerPool _samplerPool;
|
||||||
|
|
||||||
private readonly GpuChannel _channel;
|
private readonly GpuChannel _channel;
|
||||||
private readonly TexturePoolCache _texturePoolCache;
|
private readonly TexturePoolCache _texturePoolCache;
|
||||||
|
private readonly SamplerPoolCache _samplerPoolCache;
|
||||||
|
|
||||||
private TexturePool _cachedTexturePool;
|
private TexturePool _cachedTexturePool;
|
||||||
private SamplerPool _cachedSamplerPool;
|
private SamplerPool _cachedSamplerPool;
|
||||||
@@ -72,14 +74,23 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The GPU context that the texture bindings manager belongs to</param>
|
/// <param name="context">The GPU context that the texture bindings manager belongs to</param>
|
||||||
/// <param name="channel">The GPU channel that the texture bindings manager belongs to</param>
|
/// <param name="channel">The GPU channel that the texture bindings manager belongs to</param>
|
||||||
/// <param name="poolCache">Texture pools cache used to get texture pools from</param>
|
/// <param name="texturePoolCache">Texture pools cache used to get texture pools from</param>
|
||||||
|
/// <param name="samplerPoolCache">Sampler pools cache used to get sampler pools from</param>
|
||||||
/// <param name="scales">Array where the scales for the currently bound textures are stored</param>
|
/// <param name="scales">Array where the scales for the currently bound textures are stored</param>
|
||||||
/// <param name="isCompute">True if the bindings manager is used for the compute engine</param>
|
/// <param name="isCompute">True if the bindings manager is used for the compute engine</param>
|
||||||
public TextureBindingsManager(GpuContext context, GpuChannel channel, TexturePoolCache poolCache, float[] scales, bool isCompute)
|
public TextureBindingsManager(
|
||||||
|
GpuContext context,
|
||||||
|
GpuChannel channel,
|
||||||
|
TexturePoolCache texturePoolCache,
|
||||||
|
SamplerPoolCache samplerPoolCache,
|
||||||
|
float[] scales,
|
||||||
|
bool isCompute)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
_texturePoolCache = poolCache;
|
_texturePoolCache = texturePoolCache;
|
||||||
|
_samplerPoolCache = samplerPoolCache;
|
||||||
|
|
||||||
_scales = scales;
|
_scales = scales;
|
||||||
_isCompute = isCompute;
|
_isCompute = isCompute;
|
||||||
|
|
||||||
@@ -173,25 +184,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <param name="samplerIndex">Type of the sampler pool indexing used for bound samplers</param>
|
/// <param name="samplerIndex">Type of the sampler pool indexing used for bound samplers</param>
|
||||||
public void SetSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex)
|
public void SetSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex)
|
||||||
{
|
{
|
||||||
if (gpuVa != 0)
|
_samplerPoolGpuVa = gpuVa;
|
||||||
{
|
_samplerPoolMaximumId = maximumId;
|
||||||
ulong address = _channel.MemoryManager.Translate(gpuVa);
|
|
||||||
|
|
||||||
if (_samplerPool != null && _samplerPool.Address == address && _samplerPool.MaximumId >= maximumId)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_samplerPool?.Dispose();
|
|
||||||
_samplerPool = new SamplerPool(_context, _channel.MemoryManager.Physical, address, maximumId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_samplerPool?.Dispose();
|
|
||||||
_samplerPool = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_samplerIndex = samplerIndex;
|
_samplerIndex = samplerIndex;
|
||||||
|
_samplerPool = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -201,18 +197,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <param name="maximumId">Maximum ID of the pool (total count minus one)</param>
|
/// <param name="maximumId">Maximum ID of the pool (total count minus one)</param>
|
||||||
public void SetTexturePool(ulong gpuVa, int maximumId)
|
public void SetTexturePool(ulong gpuVa, int maximumId)
|
||||||
{
|
{
|
||||||
if (gpuVa != 0)
|
_texturePoolGpuVa = gpuVa;
|
||||||
{
|
|
||||||
ulong address = _channel.MemoryManager.Translate(gpuVa);
|
|
||||||
|
|
||||||
_texturePoolAddress = address;
|
|
||||||
_texturePoolMaximumId = maximumId;
|
_texturePoolMaximumId = maximumId;
|
||||||
}
|
_texturePool = null;
|
||||||
else
|
|
||||||
{
|
|
||||||
_texturePoolAddress = 0;
|
|
||||||
_texturePoolMaximumId = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -222,13 +209,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <param name="samplerId">ID of the sampler</param>
|
/// <param name="samplerId">ID of the sampler</param>
|
||||||
public (Texture, Sampler) GetTextureAndSampler(int textureId, int samplerId)
|
public (Texture, Sampler) GetTextureAndSampler(int textureId, int samplerId)
|
||||||
{
|
{
|
||||||
ulong texturePoolAddress = _texturePoolAddress;
|
(TexturePool texturePool, SamplerPool samplerPool) = GetPools();
|
||||||
|
|
||||||
TexturePool texturePool = texturePoolAddress != 0
|
return (texturePool.Get(textureId), samplerPool.Get(samplerId));
|
||||||
? _texturePoolCache.FindOrCreate(_channel, texturePoolAddress, _texturePoolMaximumId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (texturePool.Get(textureId), _samplerPool.Get(samplerId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -340,13 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
||||||
public bool CommitBindings(ShaderSpecializationState specState)
|
public bool CommitBindings(ShaderSpecializationState specState)
|
||||||
{
|
{
|
||||||
ulong texturePoolAddress = _texturePoolAddress;
|
(TexturePool texturePool, SamplerPool samplerPool) = GetPools();
|
||||||
|
|
||||||
TexturePool texturePool = texturePoolAddress != 0
|
|
||||||
? _texturePoolCache.FindOrCreate(_channel, texturePoolAddress, _texturePoolMaximumId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
SamplerPool samplerPool = _samplerPool;
|
|
||||||
|
|
||||||
// Check if the texture pool has been modified since bindings were last committed.
|
// Check if the texture pool has been modified since bindings were last committed.
|
||||||
// If it wasn't, then it's possible to avoid looking up textures again when the handle remains the same.
|
// If it wasn't, then it's possible to avoid looking up textures again when the handle remains the same.
|
||||||
@@ -381,7 +358,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (_isCompute)
|
if (_isCompute)
|
||||||
{
|
{
|
||||||
specStateMatches &= CommitTextureBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState);
|
specStateMatches &= CommitTextureBindings(texturePool, samplerPool, ShaderStage.Compute, 0, poolModified, specState);
|
||||||
specStateMatches &= CommitImageBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState);
|
specStateMatches &= CommitImageBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -390,7 +367,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
int stageIndex = (int)stage - 1;
|
int stageIndex = (int)stage - 1;
|
||||||
|
|
||||||
specStateMatches &= CommitTextureBindings(texturePool, stage, stageIndex, poolModified, specState);
|
specStateMatches &= CommitTextureBindings(texturePool, samplerPool, stage, stageIndex, poolModified, specState);
|
||||||
specStateMatches &= CommitImageBindings(texturePool, stage, stageIndex, poolModified, specState);
|
specStateMatches &= CommitImageBindings(texturePool, stage, stageIndex, poolModified, specState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,13 +424,20 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// Ensures that the texture bindings are visible to the host GPU.
|
/// Ensures that the texture bindings are visible to the host GPU.
|
||||||
/// Note: this actually performs the binding using the host graphics API.
|
/// Note: this actually performs the binding using the host graphics API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pool">The current texture pool</param>
|
/// <param name="texturePool">The current texture pool</param>
|
||||||
|
/// <param name="samplerPool">The current sampler pool</param>
|
||||||
/// <param name="stage">The shader stage using the textures to be bound</param>
|
/// <param name="stage">The shader stage using the textures to be bound</param>
|
||||||
/// <param name="stageIndex">The stage number of the specified shader stage</param
|
/// <param name="stageIndex">The stage number of the specified shader stage</param
|
||||||
/// <param name="poolModified">True if either the texture or sampler pool was modified, false otherwise</param>
|
/// <param name="poolModified">True if either the texture or sampler pool was modified, false otherwise</param>
|
||||||
/// <param name="specState">Specialization state for the bound shader</param>
|
/// <param name="specState">Specialization state for the bound shader</param>
|
||||||
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
/// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns>
|
||||||
private bool CommitTextureBindings(TexturePool pool, ShaderStage stage, int stageIndex, bool poolModified, ShaderSpecializationState specState)
|
private bool CommitTextureBindings(
|
||||||
|
TexturePool texturePool,
|
||||||
|
SamplerPool samplerPool,
|
||||||
|
ShaderStage stage,
|
||||||
|
int stageIndex,
|
||||||
|
bool poolModified,
|
||||||
|
ShaderSpecializationState specState)
|
||||||
{
|
{
|
||||||
int textureCount = _textureBindingsCount[stageIndex];
|
int textureCount = _textureBindingsCount[stageIndex];
|
||||||
if (textureCount == 0)
|
if (textureCount == 0)
|
||||||
@@ -461,9 +445,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var samplerPool = _samplerPool;
|
if (texturePool == null)
|
||||||
|
|
||||||
if (pool == null)
|
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses textures, but texture pool was not set.");
|
Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses textures, but texture pool was not set.");
|
||||||
return true;
|
return true;
|
||||||
@@ -528,7 +510,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
state.TextureHandle = textureId;
|
state.TextureHandle = textureId;
|
||||||
state.SamplerHandle = samplerId;
|
state.SamplerHandle = samplerId;
|
||||||
|
|
||||||
ref readonly TextureDescriptor descriptor = ref pool.GetForBinding(textureId, out Texture texture);
|
ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(textureId, out Texture texture);
|
||||||
|
|
||||||
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
|
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
|
||||||
|
|
||||||
@@ -819,6 +801,54 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the texture and sampler pool for the GPU virtual address that are currently set.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The texture and sampler pools</returns>
|
||||||
|
private (TexturePool, SamplerPool) GetPools()
|
||||||
|
{
|
||||||
|
MemoryManager memoryManager = _channel.MemoryManager;
|
||||||
|
|
||||||
|
TexturePool texturePool = _texturePool;
|
||||||
|
SamplerPool samplerPool = _samplerPool;
|
||||||
|
|
||||||
|
if (texturePool == null)
|
||||||
|
{
|
||||||
|
ulong poolAddress = memoryManager.Translate(_texturePoolGpuVa);
|
||||||
|
|
||||||
|
if (poolAddress != MemoryManager.PteUnmapped)
|
||||||
|
{
|
||||||
|
texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId);
|
||||||
|
_texturePool = texturePool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (samplerPool == null)
|
||||||
|
{
|
||||||
|
ulong poolAddress = memoryManager.Translate(_samplerPoolGpuVa);
|
||||||
|
|
||||||
|
if (poolAddress != MemoryManager.PteUnmapped)
|
||||||
|
{
|
||||||
|
samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId);
|
||||||
|
_samplerPool = samplerPool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (texturePool, samplerPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces the texture and sampler pools to be re-loaded from the cache on next use.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This should be called if the memory mappings change, to ensure the correct pools are being used.
|
||||||
|
/// </remarks>
|
||||||
|
public void ReloadPools()
|
||||||
|
{
|
||||||
|
_samplerPool = null;
|
||||||
|
_texturePool = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Force all bound textures and images to be rebound the next time CommitBindings is called.
|
/// Force all bound textures and images to be rebound the next time CommitBindings is called.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -827,13 +857,5 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
Array.Clear(_textureState);
|
Array.Clear(_textureState);
|
||||||
Array.Clear(_imageState);
|
Array.Clear(_imageState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes all textures and samplers in the cache.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_samplerPool?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -900,23 +900,25 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null.
|
/// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="memoryManager">GPU memory manager where the texture is mapped</param>
|
/// <param name="memoryManager">GPU memory manager where the texture is mapped</param>
|
||||||
/// <param name="tex">The texture information</param>
|
|
||||||
/// <param name="gpuVa">GPU virtual address of the texture</param>
|
/// <param name="gpuVa">GPU virtual address of the texture</param>
|
||||||
/// <param name="bpp">Bytes per pixel</param>
|
/// <param name="bpp">Bytes per pixel</param>
|
||||||
/// <param name="stride">If <paramref name="linear"/> is true, should have the texture stride, otherwise ignored</param>
|
/// <param name="stride">If <paramref name="linear"/> is true, should have the texture stride, otherwise ignored</param>
|
||||||
|
/// <param name="height">If <paramref name="linear"/> is false, should have the texture height, otherwise ignored</param>
|
||||||
/// <param name="xCount">Number of pixels to be copied per line</param>
|
/// <param name="xCount">Number of pixels to be copied per line</param>
|
||||||
/// <param name="yCount">Number of lines to be copied</param>
|
/// <param name="yCount">Number of lines to be copied</param>
|
||||||
/// <param name="linear">True if the texture has a linear layout, false otherwise</param>
|
/// <param name="linear">True if the texture has a linear layout, false otherwise</param>
|
||||||
|
/// <param name="memoryLayout">If <paramref name="linear"/> is false, should have the memory layout, otherwise ignored</param>
|
||||||
/// <returns>A matching texture, or null if there is no match</returns>
|
/// <returns>A matching texture, or null if there is no match</returns>
|
||||||
public Texture FindTexture(
|
public Texture FindTexture(
|
||||||
MemoryManager memoryManager,
|
MemoryManager memoryManager,
|
||||||
DmaTexture tex,
|
|
||||||
ulong gpuVa,
|
ulong gpuVa,
|
||||||
int bpp,
|
int bpp,
|
||||||
int stride,
|
int stride,
|
||||||
|
int height,
|
||||||
int xCount,
|
int xCount,
|
||||||
int yCount,
|
int yCount,
|
||||||
bool linear)
|
bool linear,
|
||||||
|
MemoryLayout memoryLayout)
|
||||||
{
|
{
|
||||||
ulong address = memoryManager.Translate(gpuVa);
|
ulong address = memoryManager.Translate(gpuVa);
|
||||||
|
|
||||||
@@ -945,7 +947,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
// Size is not available for linear textures. Use the stride and end of the copy region instead.
|
// Size is not available for linear textures. Use the stride and end of the copy region instead.
|
||||||
|
|
||||||
match = texture.Info.IsLinear && texture.Info.Stride == stride && tex.RegionY + yCount <= texture.Info.Height;
|
match = texture.Info.IsLinear && texture.Info.Stride == stride && yCount == texture.Info.Height;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -953,10 +955,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
// Due to the way linear strided and block layouts work, widths can be multiplied by Bpp for comparison.
|
// Due to the way linear strided and block layouts work, widths can be multiplied by Bpp for comparison.
|
||||||
// Note: tex.Width is the aligned texture size. Prefer param.XCount, as the destination should be a texture with that exact size.
|
// Note: tex.Width is the aligned texture size. Prefer param.XCount, as the destination should be a texture with that exact size.
|
||||||
|
|
||||||
bool sizeMatch = xCount * bpp == texture.Info.Width * format.BytesPerPixel && tex.Height == texture.Info.Height;
|
bool sizeMatch = xCount * bpp == texture.Info.Width * format.BytesPerPixel && height == texture.Info.Height;
|
||||||
bool formatMatch = !texture.Info.IsLinear &&
|
bool formatMatch = !texture.Info.IsLinear &&
|
||||||
texture.Info.GobBlocksInY == tex.MemoryLayout.UnpackGobBlocksInY() &&
|
texture.Info.GobBlocksInY == memoryLayout.UnpackGobBlocksInY() &&
|
||||||
texture.Info.GobBlocksInZ == tex.MemoryLayout.UnpackGobBlocksInZ();
|
texture.Info.GobBlocksInZ == memoryLayout.UnpackGobBlocksInZ();
|
||||||
|
|
||||||
match = sizeMatch && formatMatch;
|
match = sizeMatch && formatMatch;
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
private readonly TextureBindingsManager _cpBindingsManager;
|
private readonly TextureBindingsManager _cpBindingsManager;
|
||||||
private readonly TextureBindingsManager _gpBindingsManager;
|
private readonly TextureBindingsManager _gpBindingsManager;
|
||||||
private readonly TexturePoolCache _texturePoolCache;
|
private readonly TexturePoolCache _texturePoolCache;
|
||||||
|
private readonly SamplerPoolCache _samplerPoolCache;
|
||||||
|
|
||||||
private readonly Texture[] _rtColors;
|
private readonly Texture[] _rtColors;
|
||||||
private readonly ITexture[] _rtHostColors;
|
private readonly ITexture[] _rtHostColors;
|
||||||
@@ -41,13 +42,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_channel = channel;
|
_channel = channel;
|
||||||
|
|
||||||
TexturePoolCache texturePoolCache = new TexturePoolCache(context);
|
TexturePoolCache texturePoolCache = new TexturePoolCache(context);
|
||||||
|
SamplerPoolCache samplerPoolCache = new SamplerPoolCache(context);
|
||||||
|
|
||||||
float[] scales = new float[64];
|
float[] scales = new float[64];
|
||||||
new Span<float>(scales).Fill(1f);
|
new Span<float>(scales).Fill(1f);
|
||||||
|
|
||||||
_cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, scales, isCompute: true);
|
_cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: true);
|
||||||
_gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, scales, isCompute: false);
|
_gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: false);
|
||||||
_texturePoolCache = texturePoolCache;
|
_texturePoolCache = texturePoolCache;
|
||||||
|
_samplerPoolCache = samplerPoolCache;
|
||||||
|
|
||||||
_rtColors = new Texture[Constants.TotalRenderTargets];
|
_rtColors = new Texture[Constants.TotalRenderTargets];
|
||||||
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
|
_rtHostColors = new ITexture[Constants.TotalRenderTargets];
|
||||||
@@ -368,6 +371,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
// we must rebind everything.
|
// we must rebind everything.
|
||||||
// Since compute work happens less often, we always do that
|
// Since compute work happens less often, we always do that
|
||||||
// before and after the compute dispatch.
|
// before and after the compute dispatch.
|
||||||
|
|
||||||
|
_texturePoolCache.Tick();
|
||||||
|
_samplerPoolCache.Tick();
|
||||||
|
|
||||||
_cpBindingsManager.Rebind();
|
_cpBindingsManager.Rebind();
|
||||||
bool result = _cpBindingsManager.CommitBindings(specState);
|
bool result = _cpBindingsManager.CommitBindings(specState);
|
||||||
_gpBindingsManager.Rebind();
|
_gpBindingsManager.Rebind();
|
||||||
@@ -382,6 +389,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <returns>True if all bound textures match the current shader specialization state, false otherwise</returns>
|
/// <returns>True if all bound textures match the current shader specialization state, false otherwise</returns>
|
||||||
public bool CommitGraphicsBindings(ShaderSpecializationState specState)
|
public bool CommitGraphicsBindings(ShaderSpecializationState specState)
|
||||||
{
|
{
|
||||||
|
_texturePoolCache.Tick();
|
||||||
|
_samplerPoolCache.Tick();
|
||||||
|
|
||||||
bool result = _gpBindingsManager.CommitBindings(specState);
|
bool result = _gpBindingsManager.CommitBindings(specState);
|
||||||
|
|
||||||
UpdateRenderTargets();
|
UpdateRenderTargets();
|
||||||
@@ -501,6 +511,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs);
|
_context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces the texture and sampler pools to be re-loaded from the cache on next use.
|
||||||
|
/// </summary>
|
||||||
|
public void ReloadPools()
|
||||||
|
{
|
||||||
|
_cpBindingsManager.ReloadPools();
|
||||||
|
_gpBindingsManager.ReloadPools();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forces all textures, samplers, images and render targets to be rebound the next time
|
/// Forces all textures, samplers, images and render targets to be rebound the next time
|
||||||
/// CommitGraphicsBindings is called.
|
/// CommitGraphicsBindings is called.
|
||||||
@@ -523,8 +542,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_cpBindingsManager.Dispose();
|
// Textures are owned by the texture cache, so we shouldn't dispose the texture pool cache.
|
||||||
_gpBindingsManager.Dispose();
|
_samplerPoolCache.Dispose();
|
||||||
|
|
||||||
for (int i = 0; i < _rtColors.Length; i++)
|
for (int i = 0; i < _rtColors.Length; i++)
|
||||||
{
|
{
|
||||||
|
@@ -10,19 +10,24 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Texture pool.
|
/// Texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TexturePool : Pool<Texture, TextureDescriptor>
|
class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool>
|
||||||
{
|
{
|
||||||
private readonly GpuChannel _channel;
|
private readonly GpuChannel _channel;
|
||||||
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
||||||
private TextureDescriptor _defaultDescriptor;
|
private TextureDescriptor _defaultDescriptor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Intrusive linked list node used on the texture pool cache.
|
/// Linked list node used on the texture pool cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LinkedListNode<TexturePool> CacheNode { get; set; }
|
public LinkedListNode<TexturePool> CacheNode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new instance of the texture pool.
|
/// Timestamp used by the texture pool cache, updated on every use of this texture pool.
|
||||||
|
/// </summary>
|
||||||
|
public ulong CacheTimestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">GPU context that the texture pool belongs to</param>
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,69 +5,26 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// This can keep multiple texture pools, and return the current one as needed.
|
/// This can keep multiple texture pools, and return the current one as needed.
|
||||||
/// It is useful for applications that uses multiple texture pools.
|
/// It is useful for applications that uses multiple texture pools.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TexturePoolCache
|
class TexturePoolCache : PoolCache<TexturePool>
|
||||||
{
|
{
|
||||||
private const int MaxCapacity = 4;
|
|
||||||
|
|
||||||
private readonly GpuContext _context;
|
|
||||||
private readonly LinkedList<TexturePool> _pools;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new instance of the texture pool.
|
/// Constructs a new instance of the texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">GPU context that the texture pool belongs to</param>
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
public TexturePoolCache(GpuContext context)
|
public TexturePoolCache(GpuContext context) : base(context)
|
||||||
{
|
{
|
||||||
_context = context;
|
|
||||||
_pools = new LinkedList<TexturePool>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds a cache texture pool, or creates a new one if not found.
|
/// Creates a new instance of the texture pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">GPU channel that the texture pool cache belongs to</param>
|
/// <param name="context">GPU context that the texture pool belongs to</param>
|
||||||
/// <param name="address">Start address of the texture pool</param>
|
/// <param name="channel">GPU channel that the texture pool belongs to</param>
|
||||||
/// <param name="maximumId">Maximum ID of the texture pool</param>
|
/// <param name="address">Address of the texture pool in guest memory</param>
|
||||||
/// <returns>The found or newly created texture pool</returns>
|
/// <param name="maximumId">Maximum texture ID of the texture pool (equal to maximum textures minus one)</param>
|
||||||
public TexturePool FindOrCreate(GpuChannel channel, ulong address, int maximumId)
|
protected override TexturePool CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId)
|
||||||
{
|
{
|
||||||
TexturePool pool;
|
return new TexturePool(context, channel, address, maximumId);
|
||||||
|
|
||||||
// First we try to find the pool.
|
|
||||||
for (LinkedListNode<TexturePool> node = _pools.First; node != null; node = node.Next)
|
|
||||||
{
|
|
||||||
pool = node.Value;
|
|
||||||
|
|
||||||
if (pool.Address == address)
|
|
||||||
{
|
|
||||||
if (pool.CacheNode != _pools.Last)
|
|
||||||
{
|
|
||||||
_pools.Remove(pool.CacheNode);
|
|
||||||
|
|
||||||
pool.CacheNode = _pools.AddLast(pool);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found, create a new one.
|
|
||||||
pool = new TexturePool(_context, channel, address, maximumId);
|
|
||||||
|
|
||||||
pool.CacheNode = _pools.AddLast(pool);
|
|
||||||
|
|
||||||
if (_pools.Count > MaxCapacity)
|
|
||||||
{
|
|
||||||
TexturePool oldestPool = _pools.First.Value;
|
|
||||||
|
|
||||||
_pools.RemoveFirst();
|
|
||||||
|
|
||||||
oldestPool.Dispose();
|
|
||||||
|
|
||||||
oldestPool.CacheNode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -422,19 +422,19 @@ namespace Ryujinx.HLE.HOS
|
|||||||
|
|
||||||
if (File.Exists(titleAocMetadataPath))
|
if (File.Exists(titleAocMetadataPath))
|
||||||
{
|
{
|
||||||
List<DlcContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(titleAocMetadataPath);
|
List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(titleAocMetadataPath);
|
||||||
|
|
||||||
foreach (DlcContainer dlcContainer in dlcContainerList)
|
foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList)
|
||||||
{
|
{
|
||||||
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
|
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||||
{
|
{
|
||||||
if (File.Exists(dlcContainer.Path))
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
{
|
{
|
||||||
_device.Configuration.ContentManager.AddAocItem(dlcNca.TitleId, dlcContainer.Path, dlcNca.Path, dlcNca.Enabled);
|
_device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath, downloadableContentNca.Enabled);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {dlcContainer.Path}. It may have been moved or renamed.");
|
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,7 @@ namespace Ryujinx.Ui.Windows
|
|||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly string _titleId;
|
private readonly string _titleId;
|
||||||
private readonly string _dlcJsonPath;
|
private readonly string _dlcJsonPath;
|
||||||
private readonly List<DlcContainer> _dlcContainerList;
|
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
||||||
|
|
||||||
#pragma warning disable CS0649, IDE0044
|
#pragma warning disable CS0649, IDE0044
|
||||||
[GUI] Label _baseTitleInfoLabel;
|
[GUI] Label _baseTitleInfoLabel;
|
||||||
@@ -45,11 +45,11 @@ namespace Ryujinx.Ui.Windows
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
|
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_dlcJsonPath);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_dlcContainerList = new List<DlcContainer>();
|
_dlcContainerList = new List<DownloadableContentContainer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
_dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string));
|
_dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string));
|
||||||
@@ -75,37 +75,37 @@ namespace Ryujinx.Ui.Windows
|
|||||||
_dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
|
_dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
|
||||||
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
|
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
|
||||||
|
|
||||||
foreach (DlcContainer dlcContainer in _dlcContainerList)
|
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
|
||||||
{
|
{
|
||||||
if (File.Exists(dlcContainer.Path))
|
if (File.Exists(dlcContainer.ContainerPath))
|
||||||
{
|
{
|
||||||
// The parent tree item has its own "enabled" check box, but it's the actual
|
// The parent tree item has its own "enabled" check box, but it's the actual
|
||||||
// nca entries that store the enabled / disabled state. A bit of a UI inconsistency.
|
// nca entries that store the enabled / disabled state. A bit of a UI inconsistency.
|
||||||
// Maybe a tri-state check box would be better, but for now we check the parent
|
// Maybe a tri-state check box would be better, but for now we check the parent
|
||||||
// "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca.
|
// "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca.
|
||||||
bool areAllContentPacksEnabled = dlcContainer.DlcNcaList.TrueForAll((nca) => nca.Enabled);
|
bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled);
|
||||||
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.Path);
|
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath);
|
||||||
using FileStream containerFile = File.OpenRead(dlcContainer.Path);
|
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
|
||||||
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
|
||||||
_virtualFileSystem.ImportTickets(pfs);
|
_virtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
|
foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
|
||||||
{
|
{
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath);
|
||||||
|
|
||||||
if (nca != null)
|
if (nca != null)
|
||||||
{
|
{
|
||||||
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.Path);
|
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.FullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog.
|
// DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog.
|
||||||
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.Path}");
|
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,19 +237,19 @@ namespace Ryujinx.Ui.Windows
|
|||||||
{
|
{
|
||||||
if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
|
if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
|
||||||
{
|
{
|
||||||
DlcContainer dlcContainer = new DlcContainer
|
DownloadableContentContainer dlcContainer = new DownloadableContentContainer
|
||||||
{
|
{
|
||||||
Path = (string)_dlcTreeView.Model.GetValue(parentIter, 2),
|
ContainerPath = (string)_dlcTreeView.Model.GetValue(parentIter, 2),
|
||||||
DlcNcaList = new List<DlcNca>()
|
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||||
};
|
};
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
dlcContainer.DlcNcaList.Add(new DlcNca
|
dlcContainer.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||||
{
|
{
|
||||||
Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0),
|
Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0),
|
||||||
TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16),
|
TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16),
|
||||||
Path = (string)_dlcTreeView.Model.GetValue(childIter, 2)
|
FullPath = (string)_dlcTreeView.Model.GetValue(childIter, 2)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
while (_dlcTreeView.Model.IterNext(ref childIter));
|
while (_dlcTreeView.Model.IterNext(ref childIter));
|
||||||
|
Reference in New Issue
Block a user