Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1fc90e57d2 | ||
|
eafcc314a9 |
@@ -583,10 +583,10 @@
|
||||
"SelectUpdateDialogTitle": "Select update files",
|
||||
"UserProfileWindowTitle": "User Profiles Manager",
|
||||
"CheatWindowTitle": "Cheats Manager",
|
||||
"DlcWindowTitle": "Downloadable Content Manager",
|
||||
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||
"UpdateWindowTitle": "Title Update Manager",
|
||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||
"DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
|
||||
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
||||
"UserProfilesEditProfile": "Edit Selected",
|
||||
"Cancel": "Cancel",
|
||||
"Save": "Save",
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
@@ -21,6 +22,8 @@ namespace Ryujinx.Ava.UI.Models
|
||||
public string ContainerPath { get; }
|
||||
public string FullPath { get; }
|
||||
|
||||
public string FileName => Path.GetFileName(ContainerPath);
|
||||
|
||||
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||
{
|
||||
TitleId = titleId;
|
||||
|
340
Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
Normal file
340
Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
Normal file
@@ -0,0 +1,340 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
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.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
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.ViewModels
|
||||
{
|
||||
public class DownloadableContentManagerViewModel : BaseModel
|
||||
{
|
||||
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||
private readonly string _downloadableContentJsonPath;
|
||||
|
||||
private VirtualFileSystem _virtualFileSystem;
|
||||
private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
|
||||
private AvaloniaList<DownloadableContentModel> _views = new();
|
||||
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
||||
|
||||
private string _search;
|
||||
private ulong _titleId;
|
||||
private string _titleName;
|
||||
|
||||
public AvaloniaList<DownloadableContentModel> DownloadableContents
|
||||
{
|
||||
get => _downloadableContents;
|
||||
set
|
||||
{
|
||||
_downloadableContents = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public AvaloniaList<DownloadableContentModel> Views
|
||||
{
|
||||
get => _views;
|
||||
set
|
||||
{
|
||||
_views = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public AvaloniaList<DownloadableContentModel> SelectedDownloadableContents
|
||||
{
|
||||
get => _selectedDownloadableContents;
|
||||
set
|
||||
{
|
||||
_selectedDownloadableContents = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Search
|
||||
{
|
||||
get => _search;
|
||||
set
|
||||
{
|
||||
_search = value;
|
||||
OnPropertyChanged();
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public string UpdateCount
|
||||
{
|
||||
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
|
||||
}
|
||||
|
||||
public DownloadableContentManagerViewModel(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
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize.");
|
||||
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||
}
|
||||
|
||||
LoadDownloadableContents();
|
||||
}
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||
{
|
||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||
{
|
||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||
if (nca != null)
|
||||
{
|
||||
var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||
downloadableContentContainer.ContainerPath,
|
||||
downloadableContentNca.FullPath,
|
||||
downloadableContentNca.Enabled);
|
||||
|
||||
DownloadableContents.Add(content);
|
||||
|
||||
if (content.Enabled)
|
||||
{
|
||||
SelectedDownloadableContents.Add(content);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Save the list again to remove leftovers.
|
||||
Save();
|
||||
Sort();
|
||||
}
|
||||
|
||||
public void Sort()
|
||||
{
|
||||
DownloadableContents.AsObservableChangeSet()
|
||||
.Filter(Filter)
|
||||
.Bind(out var view).AsObservableList();
|
||||
|
||||
_views.Clear();
|
||||
_views.AddRange(view);
|
||||
OnPropertyChanged(nameof(Views));
|
||||
}
|
||||
|
||||
private bool Filter(object arg)
|
||||
{
|
||||
if (arg is DownloadableContentModel content)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleId.ToLower().Contains(_search.ToLower());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Nca TryOpenNca(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[LocaleKeys.DialogLoadNcaErrorMessage], ex.Message, containerPath));
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
|
||||
AllowMultiple = true
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = "NSP",
|
||||
Extensions = { "nsp" }
|
||||
});
|
||||
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
string[] files = await dialog.ShowAsync(desktop.MainWindow);
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
foreach (string file in files)
|
||||
{
|
||||
await AddDownloadableContent(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 partitionFileSystem = new(containerFile.AsStorage());
|
||||
bool containsDownloadableContent = false;
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
|
||||
if (nca == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true);
|
||||
DownloadableContents.Add(content);
|
||||
SelectedDownloadableContents.Add(content);
|
||||
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
|
||||
containsDownloadableContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDownloadableContent)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(DownloadableContentModel model)
|
||||
{
|
||||
DownloadableContents.Remove(model);
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
DownloadableContents.Clear();
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
}
|
||||
|
||||
public void EnableAll()
|
||||
{
|
||||
SelectedDownloadableContents = new(DownloadableContents);
|
||||
}
|
||||
|
||||
public void DisableAll()
|
||||
{
|
||||
SelectedDownloadableContents.Clear();
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1564,7 +1564,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
if (SelectedApplication != null)
|
||||
{
|
||||
await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
|
||||
await DownloadableContentManagerWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,172 +1,194 @@
|
||||
<window:StyleableWindow
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Windows.DownloadableContentManagerWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="800"
|
||||
Height="500"
|
||||
MinWidth="800"
|
||||
MinHeight="500"
|
||||
MaxWidth="800"
|
||||
MaxHeight="500"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
Width="500"
|
||||
Height="380"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:DownloadableContentManagerViewModel"
|
||||
Focusable="True">
|
||||
<Grid Name="DownloadableContentGrid" Margin="15">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Name="Heading"
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="18"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<DockPanel
|
||||
Grid.Row="2"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="EnableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding EnableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="DisableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding DisableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<Panel
|
||||
Margin="0 0 0 10"
|
||||
Grid.Row="0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Text="{Binding UpdateCount}" />
|
||||
<StackPanel
|
||||
Margin="10 0"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Name="EnableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{ReflectionBinding EnableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="DisableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{ReflectionBinding DisableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<TextBox
|
||||
Grid.Column="2"
|
||||
MinHeight="27"
|
||||
MaxHeight="27"
|
||||
HorizontalAlignment="Stretch"
|
||||
Watermark="{locale:Locale Search}"
|
||||
Text="{Binding Search}" />
|
||||
</Grid>
|
||||
</Panel>
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
Grid.Row="1"
|
||||
Margin="0 0 0 24"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<ScrollViewer
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid
|
||||
Name="DlcDataGrid"
|
||||
MinHeight="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
Items="{Binding _downloadableContents}"
|
||||
SelectionMode="Extended"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid.Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
</Style>
|
||||
</Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(1)">
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
</Styles>
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="90">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
Width="50"
|
||||
MinWidth="40"
|
||||
HorizontalAlignment="Center"
|
||||
IsChecked="{Binding Enabled}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
|
||||
</DataGridTemplateColumn.Header>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Width="140" Binding="{Binding TitleId}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Width="280" Binding="{Binding FullPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Binding="{Binding ContainerPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</ScrollViewer>
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Padding="2.5">
|
||||
<ListBox
|
||||
AutoScrollToSelectedItem="False"
|
||||
VirtualizationMode="None"
|
||||
SelectionMode="Multiple, Toggle"
|
||||
Background="Transparent"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}"
|
||||
Items="{Binding Views}">
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate
|
||||
DataType="models:DownloadableContentModel">
|
||||
<Panel Margin="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid
|
||||
Grid.Column="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
MaxLines="2"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding FileName}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="10 0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding TitleId}" />
|
||||
</Grid>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Click="OpenLocation">
|
||||
<ui:SymbolIcon
|
||||
Symbol="OpenFolder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Click="RemoveDLC">
|
||||
<ui:SymbolIcon
|
||||
Symbol="Cancel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
</ListBox>
|
||||
</Border>
|
||||
<DockPanel
|
||||
Grid.Row="4"
|
||||
Margin="0"
|
||||
<Panel
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="AddButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Add}">
|
||||
Command="{ReflectionBinding Add}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveSelected}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveAll}">
|
||||
Command="{ReflectionBinding RemoveAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Right">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding SaveAndClose}">
|
||||
Click="SaveAndClose">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Close}">
|
||||
Click="Close">
|
||||
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
||||
</UserControl>
|
@@ -1,314 +1,115 @@
|
||||
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 Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Threading.Tasks;
|
||||
using Path = System.IO.Path;
|
||||
using Button = Avalonia.Controls.Button;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class DownloadableContentManagerWindow : StyleableWindow
|
||||
public partial class DownloadableContentManagerWindow : UserControl
|
||||
{
|
||||
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||
private readonly string _downloadableContentJsonPath;
|
||||
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; }
|
||||
|
||||
private ulong _titleId { get; }
|
||||
private string _titleName { get; }
|
||||
public DownloadableContentManagerViewModel ViewModel;
|
||||
|
||||
public DownloadableContentManagerWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
|
||||
}
|
||||
|
||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_downloadableContents = new AvaloniaList<DownloadableContentModel>();
|
||||
|
||||
_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;
|
||||
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId, titleName);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
RemoveButton.IsEnabled = false;
|
||||
|
||||
DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged;
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
|
||||
|
||||
LoadDownloadableContents();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0);
|
||||
}
|
||||
|
||||
private void PrintHeading()
|
||||
{
|
||||
Heading.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DlcWindowHeading, _downloadableContents.Count, _titleName, _titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||
{
|
||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(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 TryOpenNca(IStorage ncaStorage, string containerPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, 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 partitionFileSystem = new(containerFile.AsStorage());
|
||||
bool containsDownloadableContent = false;
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(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[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDownloadableContents(bool removeSelectedOnly = false)
|
||||
{
|
||||
if (removeSelectedOnly)
|
||||
{
|
||||
AvaloniaList<DownloadableContentModel> removedItems = new();
|
||||
|
||||
foreach (var item in DlcDataGrid.SelectedItems)
|
||||
{
|
||||
removedItems.Add(item as DownloadableContentModel);
|
||||
}
|
||||
|
||||
DlcDataGrid.SelectedItems.Clear();
|
||||
|
||||
foreach (var item in removedItems)
|
||||
{
|
||||
_downloadableContents.RemoveAll(_downloadableContents.Where(x => x.TitleId == item.TitleId).ToList());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_downloadableContents.Clear();
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void RemoveSelected()
|
||||
{
|
||||
RemoveDownloadableContents(true);
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
RemoveDownloadableContents();
|
||||
}
|
||||
|
||||
public void EnableAll()
|
||||
{
|
||||
foreach(var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableAll()
|
||||
{
|
||||
foreach (var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
|
||||
AllowMultiple = true
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = "",
|
||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId, titleName),
|
||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16"))
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = "NSP",
|
||||
Extensions = { "nsp" }
|
||||
});
|
||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||
bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
|
||||
|
||||
string[] files = await dialog.ShowAsync(this);
|
||||
contentDialog.Styles.Add(bottomBorder);
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
foreach (string file in files)
|
||||
{
|
||||
await AddDownloadableContent(file);
|
||||
}
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs)
|
||||
{
|
||||
_downloadableContentContainerList.Clear();
|
||||
ViewModel.Save();
|
||||
((ContentDialog)Parent).Hide();
|
||||
}
|
||||
|
||||
DownloadableContentContainer container = default;
|
||||
private void Close(object sender, RoutedEventArgs e)
|
||||
{
|
||||
((ContentDialog)Parent).Hide();
|
||||
}
|
||||
|
||||
foreach (DownloadableContentModel downloadableContent in _downloadableContents)
|
||||
private void RemoveDLC(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (container.ContainerPath != downloadableContent.ContainerPath)
|
||||
if (button.DataContext is DownloadableContentModel model)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
ViewModel.Remove(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenLocation(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
if (button.DataContext is DownloadableContentModel model)
|
||||
{
|
||||
OpenHelper.LocateFile(model.ContainerPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
foreach (var content in e.AddedItems)
|
||||
{
|
||||
if (content is DownloadableContentModel model)
|
||||
{
|
||||
var index = ViewModel.DownloadableContents.IndexOf(model);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
ViewModel.DownloadableContents[index].Enabled = true;
|
||||
}
|
||||
|
||||
container = new DownloadableContentContainer
|
||||
{
|
||||
ContainerPath = downloadableContent.ContainerPath,
|
||||
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
container.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||
foreach (var content in e.RemovedItems)
|
||||
{
|
||||
if (content is DownloadableContentModel model)
|
||||
{
|
||||
Enabled = downloadableContent.Enabled,
|
||||
TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
|
||||
FullPath = downloadableContent.FullPath
|
||||
});
|
||||
}
|
||||
var index = ViewModel.DownloadableContents.IndexOf(model);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
if (index != -1)
|
||||
{
|
||||
ViewModel.DownloadableContents[index].Enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveAndClose()
|
||||
{
|
||||
Save();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
@@ -28,5 +28,10 @@ namespace Ryujinx.Cpu.Tracking
|
||||
public void Reprotect(bool asDirty = false) => _impl.Reprotect(asDirty);
|
||||
|
||||
public bool OverlapsWith(ulong address, ulong size) => _impl.OverlapsWith(address, size);
|
||||
|
||||
public bool RangeEquals(CpuRegionHandle other)
|
||||
{
|
||||
return _impl.RealAddress == other._impl.RealAddress && _impl.RealSize == other._impl.RealSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,5 +20,15 @@ namespace Ryujinx.Graphics.GAL
|
||||
{
|
||||
return target == Target.Texture2DMultisample || target == Target.Texture2DMultisampleArray;
|
||||
}
|
||||
|
||||
public static bool HasDepthOrLayers(this Target target)
|
||||
{
|
||||
return target == Target.Texture3D ||
|
||||
target == Target.Texture1DArray ||
|
||||
target == Target.Texture2DArray ||
|
||||
target == Target.Texture2DMultisampleArray ||
|
||||
target == Target.Cubemap ||
|
||||
target == Target.CubemapArray;
|
||||
}
|
||||
}
|
||||
}
|
@@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
public TexturePool Pool;
|
||||
public int ID;
|
||||
public ulong GpuAddress;
|
||||
}
|
||||
|
||||
private GpuContext _context;
|
||||
@@ -162,6 +163,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
public bool IsView => _viewStorage != this;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this texture has views.
|
||||
/// </summary>
|
||||
public bool HasViews => _views.Count > 0;
|
||||
|
||||
private int _referenceCount;
|
||||
private List<TexturePoolOwner> _poolOwners;
|
||||
|
||||
@@ -383,6 +389,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
DecrementReferenceCount();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the texture's physical memory range. This forces tracking to regenerate.
|
||||
/// </summary>
|
||||
/// <param name="range">New physical memory range backing the texture</param>
|
||||
public void ReplaceRange(MultiRange range)
|
||||
{
|
||||
Range = range;
|
||||
|
||||
Group.RangeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy dependency to a texture that is view compatible with this one.
|
||||
/// When either texture is modified, the texture data will be copied to the other to keep them in sync.
|
||||
@@ -715,6 +732,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
height = Math.Max(height >> level, 1);
|
||||
depth = Math.Max(depth >> level, 1);
|
||||
|
||||
int sliceDepth = single ? 1 : depth;
|
||||
|
||||
SpanOrArray<byte> result;
|
||||
|
||||
if (Info.IsLinear)
|
||||
@@ -735,7 +754,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
single ? 1 : depth,
|
||||
sliceDepth,
|
||||
levels,
|
||||
layers,
|
||||
Info.FormatInfo.BlockWidth,
|
||||
@@ -759,7 +778,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
Info.FormatInfo.BlockHeight,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
sliceDepth,
|
||||
levels,
|
||||
layers,
|
||||
out byte[] decoded))
|
||||
@@ -771,7 +790,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
if (GraphicsConfig.EnableTextureRecompression)
|
||||
{
|
||||
decoded = BCnEncoder.EncodeBC7(decoded, width, height, depth, levels, layers);
|
||||
decoded = BCnEncoder.EncodeBC7(decoded, width, height, sliceDepth, levels, layers);
|
||||
}
|
||||
|
||||
result = decoded;
|
||||
@@ -782,15 +801,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
case Format.Etc2RgbaSrgb:
|
||||
case Format.Etc2RgbaUnorm:
|
||||
result = ETC2Decoder.DecodeRgba(result, width, height, depth, levels, layers);
|
||||
result = ETC2Decoder.DecodeRgba(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Etc2RgbPtaSrgb:
|
||||
case Format.Etc2RgbPtaUnorm:
|
||||
result = ETC2Decoder.DecodePta(result, width, height, depth, levels, layers);
|
||||
result = ETC2Decoder.DecodePta(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Etc2RgbSrgb:
|
||||
case Format.Etc2RgbUnorm:
|
||||
result = ETC2Decoder.DecodeRgb(result, width, height, depth, levels, layers);
|
||||
result = ETC2Decoder.DecodeRgb(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -800,31 +819,31 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
case Format.Bc1RgbaSrgb:
|
||||
case Format.Bc1RgbaUnorm:
|
||||
result = BCnDecoder.DecodeBC1(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC1(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Bc2Srgb:
|
||||
case Format.Bc2Unorm:
|
||||
result = BCnDecoder.DecodeBC2(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC2(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Bc3Srgb:
|
||||
case Format.Bc3Unorm:
|
||||
result = BCnDecoder.DecodeBC3(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC3(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
case Format.Bc4Snorm:
|
||||
case Format.Bc4Unorm:
|
||||
result = BCnDecoder.DecodeBC4(result, width, height, depth, levels, layers, Format == Format.Bc4Snorm);
|
||||
result = BCnDecoder.DecodeBC4(result, width, height, sliceDepth, levels, layers, Format == Format.Bc4Snorm);
|
||||
break;
|
||||
case Format.Bc5Snorm:
|
||||
case Format.Bc5Unorm:
|
||||
result = BCnDecoder.DecodeBC5(result, width, height, depth, levels, layers, Format == Format.Bc5Snorm);
|
||||
result = BCnDecoder.DecodeBC5(result, width, height, sliceDepth, levels, layers, Format == Format.Bc5Snorm);
|
||||
break;
|
||||
case Format.Bc6HSfloat:
|
||||
case Format.Bc6HUfloat:
|
||||
result = BCnDecoder.DecodeBC6(result, width, height, depth, levels, layers, Format == Format.Bc6HSfloat);
|
||||
result = BCnDecoder.DecodeBC6(result, width, height, sliceDepth, levels, layers, Format == Format.Bc6HSfloat);
|
||||
break;
|
||||
case Format.Bc7Srgb:
|
||||
case Format.Bc7Unorm:
|
||||
result = BCnDecoder.DecodeBC7(result, width, height, depth, levels, layers);
|
||||
result = BCnDecoder.DecodeBC7(result, width, height, sliceDepth, levels, layers);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1484,11 +1503,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
/// <param name="pool">The texture pool this texture has been added to</param>
|
||||
/// <param name="id">The ID of the reference to this texture in the pool</param>
|
||||
public void IncrementReferenceCount(TexturePool pool, int id)
|
||||
/// <param name="gpuVa">GPU VA of the pool reference</param>
|
||||
public void IncrementReferenceCount(TexturePool pool, int id, ulong gpuVa)
|
||||
{
|
||||
lock (_poolOwners)
|
||||
{
|
||||
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id });
|
||||
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id, GpuAddress = gpuVa });
|
||||
}
|
||||
_referenceCount++;
|
||||
|
||||
@@ -1585,6 +1605,36 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
InvalidatedSequence++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updating texture mappings on the pool. Happens from another thread.
|
||||
/// </summary>
|
||||
public void UpdatePoolMappings()
|
||||
{
|
||||
lock (_poolOwners)
|
||||
{
|
||||
ulong address = 0;
|
||||
|
||||
foreach (var owner in _poolOwners)
|
||||
{
|
||||
if (address == 0 || address == owner.GpuAddress)
|
||||
{
|
||||
address = owner.GpuAddress;
|
||||
|
||||
owner.Pool.QueueUpdateMapping(this, owner.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there is a different GPU VA mapping, prefer the first and delete the others.
|
||||
owner.Pool.ForceRemove(this, owner.ID, true);
|
||||
}
|
||||
}
|
||||
|
||||
_poolOwners.Clear();
|
||||
}
|
||||
|
||||
InvalidatedSequence++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the texture if it is not used anymore.
|
||||
/// The texture is considered unused when the reference count is zero,
|
||||
@@ -1636,7 +1686,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
Group.ClearModified(unmapRange);
|
||||
}
|
||||
|
||||
RemoveFromPools(true);
|
||||
UpdatePoolMappings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -194,6 +194,39 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_cache.Lift(texture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to update a texture's physical memory range.
|
||||
/// Returns false if there is an existing texture that matches with the updated range.
|
||||
/// </summary>
|
||||
/// <param name="texture">Texture to update</param>
|
||||
/// <param name="range">New physical memory range</param>
|
||||
/// <returns>True if the mapping was updated, false otherwise</returns>
|
||||
public bool UpdateMapping(Texture texture, MultiRange range)
|
||||
{
|
||||
// There cannot be an existing texture compatible with this mapping in the texture cache already.
|
||||
int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps);
|
||||
|
||||
for (int i = 0; i < overlapCount; i++)
|
||||
{
|
||||
var other = _textureOverlaps[i];
|
||||
|
||||
if (texture != other &&
|
||||
(texture.IsViewCompatible(other.Info, other.Range, true, other.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible ||
|
||||
other.IsViewCompatible(texture.Info, texture.Range, true, texture.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_textures.Remove(texture);
|
||||
|
||||
texture.ReplaceRange(range);
|
||||
|
||||
_textures.Add(texture);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find an existing texture, or create a new one if not found.
|
||||
/// </summary>
|
||||
|
@@ -39,6 +39,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
class TextureGroup : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Threshold of layers to force granular handles (and thus partial loading) on array/3D textures.
|
||||
/// </summary>
|
||||
private const int GranularLayerThreshold = 8;
|
||||
|
||||
private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false);
|
||||
|
||||
/// <summary>
|
||||
@@ -116,7 +121,29 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_allOffsets = size.AllOffsets;
|
||||
_sliceSizes = size.SliceSizes;
|
||||
|
||||
(_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
|
||||
if (Storage.Target.HasDepthOrLayers() && Storage.Info.GetSlices() > GranularLayerThreshold)
|
||||
{
|
||||
_hasLayerViews = true;
|
||||
_hasMipViews = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
(_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
|
||||
|
||||
// If the texture is partially mapped, fully subdivide handles immediately.
|
||||
|
||||
MultiRange range = Storage.Range;
|
||||
for (int i = 0; i < range.Count; i++)
|
||||
{
|
||||
if (range.GetSubRange(i).Address == MemoryManager.PteUnmapped)
|
||||
{
|
||||
_hasLayerViews = true;
|
||||
_hasMipViews = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecalculateHandleRegions();
|
||||
}
|
||||
@@ -249,7 +276,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
bool dirty = false;
|
||||
bool anyModified = false;
|
||||
bool anyUnmapped = false;
|
||||
bool anyNotDirty = false;
|
||||
|
||||
for (int i = 0; i < regionCount; i++)
|
||||
{
|
||||
@@ -294,20 +321,21 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
dirty |= handleDirty;
|
||||
}
|
||||
|
||||
anyUnmapped |= handleUnmapped;
|
||||
|
||||
if (group.NeedsCopy)
|
||||
{
|
||||
// The texture we copied from is still being written to. Copy from it again the next time this texture is used.
|
||||
texture.SignalGroupDirty();
|
||||
}
|
||||
|
||||
_loadNeeded[baseHandle + i] = handleDirty && !handleUnmapped;
|
||||
bool loadNeeded = handleDirty && !handleUnmapped;
|
||||
|
||||
anyNotDirty |= !loadNeeded;
|
||||
_loadNeeded[baseHandle + i] = loadNeeded;
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
{
|
||||
if (anyUnmapped || (_handles.Length > 1 && (anyModified || split)))
|
||||
if (anyNotDirty || (_handles.Length > 1 && (anyModified || split)))
|
||||
{
|
||||
// Partial texture invalidation. Only update the layers/levels with dirty flags of the storage.
|
||||
|
||||
@@ -331,24 +359,56 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="regionCount">The number of handles to synchronize</param>
|
||||
private void SynchronizePartial(int baseHandle, int regionCount)
|
||||
{
|
||||
int spanEndIndex = -1;
|
||||
int spanBase = 0;
|
||||
ReadOnlySpan<byte> dataSpan = ReadOnlySpan<byte>.Empty;
|
||||
|
||||
for (int i = 0; i < regionCount; i++)
|
||||
{
|
||||
if (_loadNeeded[baseHandle + i])
|
||||
{
|
||||
var info = GetHandleInformation(baseHandle + i);
|
||||
|
||||
// Ensure the data for this handle is loaded in the span.
|
||||
if (spanEndIndex <= i - 1)
|
||||
{
|
||||
spanEndIndex = i;
|
||||
|
||||
if (_is3D)
|
||||
{
|
||||
// Look ahead to see how many handles need to be loaded.
|
||||
for (int j = i + 1; j < regionCount; j++)
|
||||
{
|
||||
if (_loadNeeded[baseHandle + j])
|
||||
{
|
||||
spanEndIndex = j;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var endInfo = spanEndIndex == i ? info : GetHandleInformation(baseHandle + spanEndIndex);
|
||||
|
||||
spanBase = _allOffsets[info.Index];
|
||||
int spanLast = _allOffsets[endInfo.Index + endInfo.Layers * endInfo.Levels - 1];
|
||||
int endOffset = Math.Min(spanLast + _sliceSizes[endInfo.BaseLevel + endInfo.Levels - 1], (int)Storage.Size);
|
||||
int size = endOffset - spanBase;
|
||||
|
||||
dataSpan = _physicalMemory.GetSpan(Storage.Range.GetSlice((ulong)spanBase, (ulong)size));
|
||||
}
|
||||
|
||||
// Only one of these will be greater than 1, as partial sync is only called when there are sub-image views.
|
||||
for (int layer = 0; layer < info.Layers; layer++)
|
||||
{
|
||||
for (int level = 0; level < info.Levels; level++)
|
||||
{
|
||||
int offsetIndex = GetOffsetIndex(info.BaseLayer + layer, info.BaseLevel + level);
|
||||
|
||||
int offset = _allOffsets[offsetIndex];
|
||||
int endOffset = Math.Min(offset + _sliceSizes[info.BaseLevel + level], (int)Storage.Size);
|
||||
int size = endOffset - offset;
|
||||
|
||||
ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Storage.Range.GetSlice((ulong)offset, (ulong)size));
|
||||
ReadOnlySpan<byte> data = dataSpan.Slice(offset - spanBase);
|
||||
|
||||
SpanOrArray<byte> result = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel + level, true);
|
||||
|
||||
@@ -865,8 +925,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <returns>A TextureGroupHandle covering the given views</returns>
|
||||
private TextureGroupHandle GenerateHandles(int viewStart, int views)
|
||||
{
|
||||
int viewEnd = viewStart + views - 1;
|
||||
(_, int lastLevel) = GetLayerLevelForView(viewEnd);
|
||||
|
||||
int offset = _allOffsets[viewStart];
|
||||
int endOffset = (viewStart + views == _allOffsets.Length) ? (int)Storage.Size : _allOffsets[viewStart + views];
|
||||
int endOffset = _allOffsets[viewEnd] + _sliceSizes[lastLevel];
|
||||
int size = endOffset - offset;
|
||||
|
||||
var result = new List<CpuRegionHandle>();
|
||||
@@ -1057,7 +1120,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// The dirty flags from the previous handles will be kept.
|
||||
/// </summary>
|
||||
/// <param name="handles">The handles to replace the current handles with</param>
|
||||
private void ReplaceHandles(TextureGroupHandle[] handles)
|
||||
/// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param>
|
||||
private void ReplaceHandles(TextureGroupHandle[] handles, bool rangeChanged)
|
||||
{
|
||||
if (_handles != null)
|
||||
{
|
||||
@@ -1065,9 +1129,50 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
foreach (TextureGroupHandle groupHandle in handles)
|
||||
{
|
||||
foreach (CpuRegionHandle handle in groupHandle.Handles)
|
||||
if (rangeChanged)
|
||||
{
|
||||
handle.Reprotect();
|
||||
// When the storage range changes, this becomes a little different.
|
||||
// If a range does not match one in the original, treat it as modified.
|
||||
// It has been newly mapped and its data must be synchronized.
|
||||
|
||||
if (groupHandle.Handles.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var oldGroup in _handles)
|
||||
{
|
||||
if (!groupHandle.OverlapsWith(oldGroup.Offset, oldGroup.Size))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (CpuRegionHandle handle in groupHandle.Handles)
|
||||
{
|
||||
bool hasMatch = false;
|
||||
|
||||
foreach (var oldHandle in oldGroup.Handles)
|
||||
{
|
||||
if (oldHandle.RangeEquals(handle))
|
||||
{
|
||||
hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMatch)
|
||||
{
|
||||
handle.Reprotect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (CpuRegionHandle handle in groupHandle.Handles)
|
||||
{
|
||||
handle.Reprotect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1089,7 +1194,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <summary>
|
||||
/// Recalculate handle regions for this texture group, and inherit existing state into the new handles.
|
||||
/// </summary>
|
||||
private void RecalculateHandleRegions()
|
||||
/// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param>
|
||||
private void RecalculateHandleRegions(bool rangeChanged = false)
|
||||
{
|
||||
TextureGroupHandle[] handles;
|
||||
|
||||
@@ -1171,7 +1277,21 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceHandles(handles);
|
||||
ReplaceHandles(handles, rangeChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates handles when the storage range has been remapped.
|
||||
/// This forces the regions to be fully subdivided.
|
||||
/// </summary>
|
||||
public void RangeChanged()
|
||||
{
|
||||
_hasLayerViews = true;
|
||||
_hasMipViews = true;
|
||||
|
||||
RecalculateHandleRegions(true);
|
||||
|
||||
SignalAllDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -1,9 +1,12 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu.Memory;
|
||||
using Ryujinx.Graphics.Texture;
|
||||
using Ryujinx.Memory.Range;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
@@ -12,8 +15,63 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// </summary>
|
||||
class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool>
|
||||
{
|
||||
/// <summary>
|
||||
/// A request to dereference a texture from a pool.
|
||||
/// </summary>
|
||||
private struct DereferenceRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the dereference is due to a mapping change or not.
|
||||
/// </summary>
|
||||
public readonly bool IsRemapped;
|
||||
|
||||
/// <summary>
|
||||
/// The texture being dereferenced.
|
||||
/// </summary>
|
||||
public readonly Texture Texture;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the pool entry this reference belonged to.
|
||||
/// </summary>
|
||||
public readonly int ID;
|
||||
|
||||
/// <summary>
|
||||
/// Create a dereference request for a texture with a specific pool ID, and remapped flag.
|
||||
/// </summary>
|
||||
/// <param name="isRemapped">Whether the dereference is due to a mapping change or not</param>
|
||||
/// <param name="texture">The texture being dereferenced</param>
|
||||
/// <param name="id">The ID of the pool entry, used to restore remapped textures</param>
|
||||
private DereferenceRequest(bool isRemapped, Texture texture, int id)
|
||||
{
|
||||
IsRemapped = isRemapped;
|
||||
Texture = texture;
|
||||
ID = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dereference request for a texture removal.
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture being removed</param>
|
||||
/// <returns>A texture removal dereference request</returns>
|
||||
public static DereferenceRequest Remove(Texture texture)
|
||||
{
|
||||
return new DereferenceRequest(false, texture, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dereference request for a texture remapping with a specific pool ID.
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture being remapped</param>
|
||||
/// <param name="id">The ID of the pool entry, used to restore remapped textures</param>
|
||||
/// <returns>A remap dereference request</returns>
|
||||
public static DereferenceRequest Remap(Texture texture, int id)
|
||||
{
|
||||
return new DereferenceRequest(true, texture, id);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly GpuChannel _channel;
|
||||
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
||||
private readonly ConcurrentQueue<DereferenceRequest> _dereferenceQueue = new ConcurrentQueue<DereferenceRequest>();
|
||||
private TextureDescriptor _defaultDescriptor;
|
||||
|
||||
/// <summary>
|
||||
@@ -58,7 +116,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
{
|
||||
TextureInfo info = GetInfo(descriptor, out int layerSize);
|
||||
|
||||
ProcessDereferenceQueue();
|
||||
// The dereference queue can put our texture back on the cache.
|
||||
if ((texture = ProcessDereferenceQueue(id)) != null)
|
||||
{
|
||||
return ref descriptor;
|
||||
}
|
||||
|
||||
texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize);
|
||||
|
||||
@@ -69,10 +131,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
texture.IncrementReferenceCount(this, id);
|
||||
|
||||
Items[id] = texture;
|
||||
|
||||
texture.IncrementReferenceCount(this, id, descriptor.UnpackAddress());
|
||||
|
||||
DescriptorCache[id] = descriptor;
|
||||
}
|
||||
else
|
||||
@@ -155,11 +217,14 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <param name="deferred">If true, queue the dereference to happen on the render thread, otherwise dereference immediately</param>
|
||||
public void ForceRemove(Texture texture, int id, bool deferred)
|
||||
{
|
||||
Items[id] = null;
|
||||
var previous = Interlocked.Exchange(ref Items[id], null);
|
||||
|
||||
if (deferred)
|
||||
{
|
||||
_dereferenceQueue.Enqueue(texture);
|
||||
if (previous != null)
|
||||
{
|
||||
_dereferenceQueue.Enqueue(DereferenceRequest.Remove(texture));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -167,16 +232,91 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues a request to update a texture's mapping.
|
||||
/// Mapping is updated later to avoid deleting the texture if it is still sparsely mapped.
|
||||
/// </summary>
|
||||
/// <param name="texture">Texture with potential mapping change</param>
|
||||
/// <param name="id">ID in cache of texture with potential mapping change</param>
|
||||
public void QueueUpdateMapping(Texture texture, int id)
|
||||
{
|
||||
if (Interlocked.Exchange(ref Items[id], null) == texture)
|
||||
{
|
||||
_dereferenceQueue.Enqueue(DereferenceRequest.Remap(texture, id));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the dereference queue, decrementing the reference count for each texture in it.
|
||||
/// This is used to ensure that texture disposal happens on the render thread.
|
||||
/// </summary>
|
||||
private void ProcessDereferenceQueue()
|
||||
/// <param name="id">The ID of the entry that triggered this method</param>
|
||||
/// <returns>Texture that matches the entry ID if it has been readded to the cache.</returns>
|
||||
private Texture ProcessDereferenceQueue(int id = -1)
|
||||
{
|
||||
while (_dereferenceQueue.TryDequeue(out Texture toRemove))
|
||||
while (_dereferenceQueue.TryDequeue(out DereferenceRequest request))
|
||||
{
|
||||
toRemove.DecrementReferenceCount();
|
||||
Texture texture = request.Texture;
|
||||
|
||||
// Unmapped storage textures can swap their ranges. The texture must be storage with no views or dependencies.
|
||||
// TODO: Would need to update ranges on views, or guarantee that ones where the range changes can be instantly deleted.
|
||||
|
||||
if (request.IsRemapped && texture.Group.Storage == texture && !texture.HasViews && !texture.Group.HasCopyDependencies)
|
||||
{
|
||||
// Has the mapping for this texture changed?
|
||||
ref readonly TextureDescriptor descriptor = ref GetDescriptorRef(request.ID);
|
||||
|
||||
ulong address = descriptor.UnpackAddress();
|
||||
|
||||
MultiRange range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size);
|
||||
|
||||
// If the texture is not mapped at all, delete its reference.
|
||||
|
||||
if (range.Count == 1 && range.GetSubRange(0).Address == MemoryManager.PteUnmapped)
|
||||
{
|
||||
texture.DecrementReferenceCount();
|
||||
continue;
|
||||
}
|
||||
|
||||
Items[request.ID] = texture;
|
||||
|
||||
// Create a new pool reference, as the last one was removed on unmap.
|
||||
|
||||
texture.IncrementReferenceCount(this, request.ID, address);
|
||||
texture.DecrementReferenceCount();
|
||||
|
||||
// Refetch the range. Changes since the last check could have been lost
|
||||
// as the cache entry was not restored (required to queue mapping change).
|
||||
|
||||
range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size);
|
||||
|
||||
if (!range.Equals(texture.Range))
|
||||
{
|
||||
// Part of the texture was mapped or unmapped. Replace the range and regenerate tracking handles.
|
||||
if (!_channel.MemoryManager.Physical.TextureCache.UpdateMapping(texture, range))
|
||||
{
|
||||
// Texture could not be remapped due to a collision, just delete it.
|
||||
if (Interlocked.Exchange(ref Items[request.ID], null) != null)
|
||||
{
|
||||
// If this is null, a request was already queued to decrement reference.
|
||||
texture.DecrementReferenceCount(this, request.ID);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.ID == id)
|
||||
{
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
texture.DecrementReferenceCount();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -213,9 +353,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
_channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor);
|
||||
}
|
||||
|
||||
texture.DecrementReferenceCount(this, id);
|
||||
|
||||
Items[id] = null;
|
||||
if (Interlocked.Exchange(ref Items[id], null) != null)
|
||||
{
|
||||
texture.DecrementReferenceCount(this, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -112,7 +112,7 @@ namespace Ryujinx.Graphics.Texture
|
||||
int outSize = GetTextureSize(
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
sliceDepth,
|
||||
levels,
|
||||
layers,
|
||||
blockWidth,
|
||||
|
@@ -8,6 +8,8 @@ namespace Ryujinx.Memory.Range
|
||||
/// </summary>
|
||||
public readonly struct MultiRange : IEquatable<MultiRange>
|
||||
{
|
||||
private const ulong InvalidAddress = ulong.MaxValue;
|
||||
|
||||
private readonly MemoryRange _singleRange;
|
||||
private readonly MemoryRange[] _ranges;
|
||||
|
||||
@@ -107,7 +109,16 @@ namespace Ryujinx.Memory.Range
|
||||
else if (offset < range.Size)
|
||||
{
|
||||
ulong sliceSize = Math.Min(size, range.Size - offset);
|
||||
ranges.Add(new MemoryRange(range.Address + offset, sliceSize));
|
||||
|
||||
if (range.Address == InvalidAddress)
|
||||
{
|
||||
ranges.Add(new MemoryRange(range.Address, sliceSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
ranges.Add(new MemoryRange(range.Address + offset, sliceSize));
|
||||
}
|
||||
|
||||
size -= sliceSize;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user