Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1fc90e57d2 | ||
|
eafcc314a9 | ||
|
6e9bd4de13 | ||
|
05a41b31bc | ||
|
eed17f963e | ||
|
c09c0c002d |
14
.github/workflows/flatpak.yml
vendored
14
.github/workflows/flatpak.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
id: version_info
|
id: version_info
|
||||||
working-directory: Ryujinx
|
working-directory: Ryujinx
|
||||||
run: |
|
run: |
|
||||||
echo "git_short_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
echo "git_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
- name: Update flatpak metadata
|
- name: Update flatpak metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
env:
|
env:
|
||||||
RYUJINX_GIT_HASH: ${{ steps.version_info.outputs.git_short_hash }}
|
RYUJINX_GIT_HASH: ${{ steps.version_info.outputs.git_hash }}
|
||||||
shell: python
|
shell: python
|
||||||
run: |
|
run: |
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -95,6 +95,16 @@ jobs:
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
# Ensure we don't destroy multiline strings
|
||||||
|
def str_presenter(dumper, data):
|
||||||
|
if len(data.splitlines()) > 1:
|
||||||
|
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
|
||||||
|
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
||||||
|
|
||||||
|
|
||||||
|
yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
|
||||||
|
|
||||||
yaml_file = "flathub/org.ryujinx.Ryujinx.yml"
|
yaml_file = "flathub/org.ryujinx.Ryujinx.yml"
|
||||||
xml_file = "flathub/org.ryujinx.Ryujinx.appdata.xml"
|
xml_file = "flathub/org.ryujinx.Ryujinx.appdata.xml"
|
||||||
|
|
||||||
|
@@ -583,10 +583,10 @@
|
|||||||
"SelectUpdateDialogTitle": "Select update files",
|
"SelectUpdateDialogTitle": "Select update files",
|
||||||
"UserProfileWindowTitle": "User Profiles Manager",
|
"UserProfileWindowTitle": "User Profiles Manager",
|
||||||
"CheatWindowTitle": "Cheats Manager",
|
"CheatWindowTitle": "Cheats Manager",
|
||||||
"DlcWindowTitle": "Downloadable Content Manager",
|
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||||
"UpdateWindowTitle": "Title Update Manager",
|
"UpdateWindowTitle": "Title Update Manager",
|
||||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||||
"DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
|
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
||||||
"UserProfilesEditProfile": "Edit Selected",
|
"UserProfilesEditProfile": "Edit Selected",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
|
||||||
<Exec Command="codesign --entitlements $(ProjectDir)..\distribution\macos\entitlements.xml -f --deep -s $(SigningCertificate) $(TargetDir)$(TargetName)" />
|
<Exec Command="codesign --entitlements '$(ProjectDir)..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
|
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Models
|
namespace Ryujinx.Ava.UI.Models
|
||||||
{
|
{
|
||||||
@@ -21,6 +22,8 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
public string ContainerPath { get; }
|
public string ContainerPath { get; }
|
||||||
public string FullPath { get; }
|
public string FullPath { get; }
|
||||||
|
|
||||||
|
public string FileName => Path.GetFileName(ContainerPath);
|
||||||
|
|
||||||
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||||
{
|
{
|
||||||
TitleId = titleId;
|
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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -236,7 +236,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public DateTimeOffset DateOffset { get; set; }
|
public DateTimeOffset DateOffset { get; set; }
|
||||||
public TimeSpan TimeOffset { get; set; }
|
public TimeSpan TimeOffset { get; set; }
|
||||||
private AvaloniaList<TimeZone> TimeZones { get; set; }
|
internal AvaloniaList<TimeZone> TimeZones { get; set; }
|
||||||
public AvaloniaList<string> GameDirectories { get; set; }
|
public AvaloniaList<string> GameDirectories { get; set; }
|
||||||
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
|
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
|
||||||
|
|
||||||
|
@@ -1,172 +1,194 @@
|
|||||||
<window:StyleableWindow
|
<UserControl
|
||||||
x:Class="Ryujinx.Ava.UI.Windows.DownloadableContentManagerWindow"
|
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"
|
||||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||||
Width="800"
|
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||||
Height="500"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
MinWidth="800"
|
Width="500"
|
||||||
MinHeight="500"
|
Height="380"
|
||||||
MaxWidth="800"
|
|
||||||
MaxHeight="500"
|
|
||||||
SizeToContent="Height"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
x:DataType="viewModels:DownloadableContentManagerViewModel"
|
||||||
Focusable="True">
|
Focusable="True">
|
||||||
<Grid Name="DownloadableContentGrid" Margin="15">
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
<Panel
|
||||||
|
Margin="0 0 0 10"
|
||||||
|
Grid.Row="0">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Name="Heading"
|
Grid.Column="0"
|
||||||
Grid.Row="1"
|
Text="{Binding UpdateCount}" />
|
||||||
MaxWidth="500"
|
<StackPanel
|
||||||
Margin="20,15,20,20"
|
Margin="10 0"
|
||||||
HorizontalAlignment="Center"
|
Grid.Column="1"
|
||||||
VerticalAlignment="Center"
|
Orientation="Horizontal">
|
||||||
LineHeight="18"
|
|
||||||
TextAlignment="Center"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
<DockPanel
|
|
||||||
Grid.Row="2"
|
|
||||||
Margin="0"
|
|
||||||
HorizontalAlignment="Left">
|
|
||||||
<Button
|
<Button
|
||||||
Name="EnableAllButton"
|
Name="EnableAllButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding EnableAll}">
|
Command="{ReflectionBinding EnableAll}">
|
||||||
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
|
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
Name="DisableAllButton"
|
Name="DisableAllButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding DisableAll}">
|
Command="{ReflectionBinding DisableAll}">
|
||||||
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
|
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
|
||||||
</Button>
|
</Button>
|
||||||
</DockPanel>
|
</StackPanel>
|
||||||
|
<TextBox
|
||||||
|
Grid.Column="2"
|
||||||
|
MinHeight="27"
|
||||||
|
MaxHeight="27"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Watermark="{locale:Locale Search}"
|
||||||
|
Text="{Binding Search}" />
|
||||||
|
</Grid>
|
||||||
|
</Panel>
|
||||||
<Border
|
<Border
|
||||||
Grid.Row="3"
|
Grid.Row="1"
|
||||||
Margin="5"
|
Margin="0 0 0 24"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
BorderBrush="Gray"
|
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||||
BorderThickness="1">
|
BorderThickness="1"
|
||||||
<ScrollViewer
|
CornerRadius="5"
|
||||||
VerticalAlignment="Stretch"
|
Padding="2.5">
|
||||||
HorizontalScrollBarVisibility="Auto"
|
<ListBox
|
||||||
VerticalScrollBarVisibility="Auto">
|
AutoScrollToSelectedItem="False"
|
||||||
<DataGrid
|
VirtualizationMode="None"
|
||||||
Name="DlcDataGrid"
|
SelectionMode="Multiple, Toggle"
|
||||||
MinHeight="200"
|
Background="Transparent"
|
||||||
HorizontalAlignment="Stretch"
|
SelectionChanged="OnSelectionChanged"
|
||||||
VerticalAlignment="Stretch"
|
SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}"
|
||||||
CanUserReorderColumns="False"
|
Items="{Binding Views}">
|
||||||
CanUserResizeColumns="True"
|
<ListBox.DataTemplates>
|
||||||
CanUserSortColumns="True"
|
<DataTemplate
|
||||||
HorizontalScrollBarVisibility="Auto"
|
DataType="models:DownloadableContentModel">
|
||||||
Items="{Binding _downloadableContents}"
|
<Panel Margin="10">
|
||||||
SelectionMode="Extended"
|
<Grid>
|
||||||
VerticalScrollBarVisibility="Auto">
|
<Grid.ColumnDefinitions>
|
||||||
<DataGrid.Styles>
|
<ColumnDefinition Width="*" />
|
||||||
<Styles>
|
<ColumnDefinition Width="Auto" />
|
||||||
<Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)">
|
</Grid.ColumnDefinitions>
|
||||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
<Grid
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
Grid.Column="0">
|
||||||
</Style>
|
<Grid.ColumnDefinitions>
|
||||||
</Styles>
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
<Styles>
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
<Style Selector="DataGridCell:nth-child(1)">
|
</Grid.ColumnDefinitions>
|
||||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
<TextBlock
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
Grid.Column="0"
|
||||||
</Style>
|
HorizontalAlignment="Left"
|
||||||
</Styles>
|
VerticalAlignment="Center"
|
||||||
</DataGrid.Styles>
|
MaxLines="2"
|
||||||
<DataGrid.Columns>
|
TextWrapping="Wrap"
|
||||||
<DataGridTemplateColumn Width="90">
|
TextTrimming="CharacterEllipsis"
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
Text="{Binding FileName}" />
|
||||||
<DataTemplate>
|
<TextBlock
|
||||||
<CheckBox
|
Grid.Column="1"
|
||||||
Width="50"
|
Margin="10 0"
|
||||||
MinWidth="40"
|
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"
|
HorizontalAlignment="Center"
|
||||||
IsChecked="{Binding Enabled}" />
|
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>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</ListBox.DataTemplates>
|
||||||
<DataGridTemplateColumn.Header>
|
<ListBox.Styles>
|
||||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
|
<Style Selector="ListBoxItem">
|
||||||
</DataGridTemplateColumn.Header>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</DataGridTemplateColumn>
|
</Style>
|
||||||
<DataGridTextColumn Width="140" Binding="{Binding TitleId}">
|
</ListBox.Styles>
|
||||||
<DataGridTextColumn.Header>
|
</ListBox>
|
||||||
<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>
|
|
||||||
</Border>
|
</Border>
|
||||||
<DockPanel
|
<Panel
|
||||||
Grid.Row="4"
|
Grid.Row="2"
|
||||||
Margin="0"
|
|
||||||
HorizontalAlignment="Stretch">
|
HorizontalAlignment="Stretch">
|
||||||
<DockPanel Margin="0" HorizontalAlignment="Left">
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="10"
|
||||||
|
HorizontalAlignment="Left">
|
||||||
<Button
|
<Button
|
||||||
Name="AddButton"
|
Name="AddButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding Add}">
|
Command="{ReflectionBinding Add}">
|
||||||
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
|
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
Name="RemoveButton"
|
|
||||||
MinWidth="90"
|
|
||||||
Margin="5"
|
|
||||||
Command="{Binding RemoveSelected}">
|
|
||||||
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
Name="RemoveAllButton"
|
Name="RemoveAllButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding RemoveAll}">
|
Command="{ReflectionBinding RemoveAll}">
|
||||||
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||||
</Button>
|
</Button>
|
||||||
</DockPanel>
|
</StackPanel>
|
||||||
<DockPanel Margin="0" HorizontalAlignment="Right">
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="10"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
<Button
|
<Button
|
||||||
Name="SaveButton"
|
Name="SaveButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding SaveAndClose}">
|
Click="SaveAndClose">
|
||||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
Name="CancelButton"
|
Name="CancelButton"
|
||||||
MinWidth="90"
|
MinWidth="90"
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Command="{Binding Close}">
|
Click="Close">
|
||||||
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||||
</Button>
|
</Button>
|
||||||
</DockPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</Panel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</window:StyleableWindow>
|
</UserControl>
|
@@ -1,314 +1,115 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Interactivity;
|
||||||
using LibHac.Common;
|
using Avalonia.Styling;
|
||||||
using LibHac.Fs;
|
using FluentAvalonia.UI.Controls;
|
||||||
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.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
|
||||||
using Ryujinx.Ava.UI.Helpers;
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Common.Utilities;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using System;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Path = System.IO.Path;
|
using Button = Avalonia.Controls.Button;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Windows
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
public partial class DownloadableContentManagerWindow : StyleableWindow
|
public partial class DownloadableContentManagerWindow : UserControl
|
||||||
{
|
{
|
||||||
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
public DownloadableContentManagerViewModel ViewModel;
|
||||||
private readonly string _downloadableContentJsonPath;
|
|
||||||
|
|
||||||
private VirtualFileSystem _virtualFileSystem { get; }
|
|
||||||
private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; }
|
|
||||||
|
|
||||||
private ulong _titleId { get; }
|
|
||||||
private string _titleName { get; }
|
|
||||||
|
|
||||||
public DownloadableContentManagerWindow()
|
public DownloadableContentManagerWindow()
|
||||||
{
|
{
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||||
{
|
{
|
||||||
_virtualFileSystem = virtualFileSystem;
|
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId, titleName);
|
||||||
_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;
|
|
||||||
|
|
||||||
InitializeComponent();
|
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);
|
ContentDialog contentDialog = new()
|
||||||
}
|
|
||||||
|
|
||||||
private void PrintHeading()
|
|
||||||
{
|
{
|
||||||
Heading.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DlcWindowHeading, _downloadableContents.Count, _titleName, _titleId.ToString("X16"));
|
PrimaryButtonText = "",
|
||||||
}
|
SecondaryButtonText = "",
|
||||||
|
CloseButtonText = "",
|
||||||
private void LoadDownloadableContents()
|
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId, titleName),
|
||||||
{
|
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16"))
|
||||||
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)
|
|
||||||
{
|
|
||||||
_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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.Filters.Add(new FileDialogFilter
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
{
|
bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
|
||||||
Name = "NSP",
|
|
||||||
Extensions = { "nsp" }
|
|
||||||
});
|
|
||||||
|
|
||||||
string[] files = await dialog.ShowAsync(this);
|
contentDialog.Styles.Add(bottomBorder);
|
||||||
|
|
||||||
if (files != null)
|
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs)
|
||||||
{
|
{
|
||||||
foreach (string file in files)
|
ViewModel.Save();
|
||||||
|
((ContentDialog)Parent).Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
await AddDownloadableContent(file);
|
((ContentDialog)Parent).Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDLC(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button button)
|
||||||
|
{
|
||||||
|
if (button.DataContext is DownloadableContentModel model)
|
||||||
|
{
|
||||||
|
ViewModel.Remove(model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintHeading();
|
private void OpenLocation(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button button)
|
||||||
|
{
|
||||||
|
if (button.DataContext is DownloadableContentModel model)
|
||||||
|
{
|
||||||
|
OpenHelper.LocateFile(model.ContainerPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
Save();
|
foreach (var content in e.AddedItems)
|
||||||
Close();
|
{
|
||||||
|
if (content is DownloadableContentModel model)
|
||||||
|
{
|
||||||
|
var index = ViewModel.DownloadableContents.IndexOf(model);
|
||||||
|
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
ViewModel.DownloadableContents[index].Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var content in e.RemovedItems)
|
||||||
|
{
|
||||||
|
if (content is DownloadableContentModel model)
|
||||||
|
{
|
||||||
|
var index = ViewModel.DownloadableContents.IndexOf(model);
|
||||||
|
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
ViewModel.DownloadableContents[index].Enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -28,5 +28,10 @@ namespace Ryujinx.Cpu.Tracking
|
|||||||
public void Reprotect(bool asDirty = false) => _impl.Reprotect(asDirty);
|
public void Reprotect(bool asDirty = false) => _impl.Reprotect(asDirty);
|
||||||
|
|
||||||
public bool OverlapsWith(ulong address, ulong size) => _impl.OverlapsWith(address, size);
|
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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -152,21 +152,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
|||||||
|
|
||||||
ulong ticks = _context.GetTimestamp();
|
ulong ticks = _context.GetTimestamp();
|
||||||
|
|
||||||
float divisor = type switch
|
|
||||||
{
|
|
||||||
ReportCounterType.SamplesPassed => _channel.TextureManager.RenderTargetScale * _channel.TextureManager.RenderTargetScale,
|
|
||||||
_ => 1f
|
|
||||||
};
|
|
||||||
|
|
||||||
ICounterEvent counter = null;
|
ICounterEvent counter = null;
|
||||||
|
|
||||||
void resultHandler(object evt, ulong result)
|
void resultHandler(object evt, ulong result)
|
||||||
{
|
{
|
||||||
if (divisor != 1f)
|
|
||||||
{
|
|
||||||
result = (ulong)MathF.Ceiling(result / divisor);
|
|
||||||
}
|
|
||||||
|
|
||||||
CounterData counterData = new CounterData
|
CounterData counterData = new CounterData
|
||||||
{
|
{
|
||||||
Counter = result,
|
Counter = result,
|
||||||
|
@@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
public TexturePool Pool;
|
public TexturePool Pool;
|
||||||
public int ID;
|
public int ID;
|
||||||
|
public ulong GpuAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GpuContext _context;
|
private GpuContext _context;
|
||||||
@@ -162,6 +163,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsView => _viewStorage != this;
|
public bool IsView => _viewStorage != this;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not this texture has views.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasViews => _views.Count > 0;
|
||||||
|
|
||||||
private int _referenceCount;
|
private int _referenceCount;
|
||||||
private List<TexturePoolOwner> _poolOwners;
|
private List<TexturePoolOwner> _poolOwners;
|
||||||
|
|
||||||
@@ -383,6 +389,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
DecrementReferenceCount();
|
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>
|
/// <summary>
|
||||||
/// Create a copy dependency to a texture that is view compatible with this one.
|
/// 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.
|
/// 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);
|
height = Math.Max(height >> level, 1);
|
||||||
depth = Math.Max(depth >> level, 1);
|
depth = Math.Max(depth >> level, 1);
|
||||||
|
|
||||||
|
int sliceDepth = single ? 1 : depth;
|
||||||
|
|
||||||
SpanOrArray<byte> result;
|
SpanOrArray<byte> result;
|
||||||
|
|
||||||
if (Info.IsLinear)
|
if (Info.IsLinear)
|
||||||
@@ -735,7 +754,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
depth,
|
depth,
|
||||||
single ? 1 : depth,
|
sliceDepth,
|
||||||
levels,
|
levels,
|
||||||
layers,
|
layers,
|
||||||
Info.FormatInfo.BlockWidth,
|
Info.FormatInfo.BlockWidth,
|
||||||
@@ -759,7 +778,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
Info.FormatInfo.BlockHeight,
|
Info.FormatInfo.BlockHeight,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
depth,
|
sliceDepth,
|
||||||
levels,
|
levels,
|
||||||
layers,
|
layers,
|
||||||
out byte[] decoded))
|
out byte[] decoded))
|
||||||
@@ -771,7 +790,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (GraphicsConfig.EnableTextureRecompression)
|
if (GraphicsConfig.EnableTextureRecompression)
|
||||||
{
|
{
|
||||||
decoded = BCnEncoder.EncodeBC7(decoded, width, height, depth, levels, layers);
|
decoded = BCnEncoder.EncodeBC7(decoded, width, height, sliceDepth, levels, layers);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = decoded;
|
result = decoded;
|
||||||
@@ -782,15 +801,15 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
case Format.Etc2RgbaSrgb:
|
case Format.Etc2RgbaSrgb:
|
||||||
case Format.Etc2RgbaUnorm:
|
case Format.Etc2RgbaUnorm:
|
||||||
result = ETC2Decoder.DecodeRgba(result, width, height, depth, levels, layers);
|
result = ETC2Decoder.DecodeRgba(result, width, height, sliceDepth, levels, layers);
|
||||||
break;
|
break;
|
||||||
case Format.Etc2RgbPtaSrgb:
|
case Format.Etc2RgbPtaSrgb:
|
||||||
case Format.Etc2RgbPtaUnorm:
|
case Format.Etc2RgbPtaUnorm:
|
||||||
result = ETC2Decoder.DecodePta(result, width, height, depth, levels, layers);
|
result = ETC2Decoder.DecodePta(result, width, height, sliceDepth, levels, layers);
|
||||||
break;
|
break;
|
||||||
case Format.Etc2RgbSrgb:
|
case Format.Etc2RgbSrgb:
|
||||||
case Format.Etc2RgbUnorm:
|
case Format.Etc2RgbUnorm:
|
||||||
result = ETC2Decoder.DecodeRgb(result, width, height, depth, levels, layers);
|
result = ETC2Decoder.DecodeRgb(result, width, height, sliceDepth, levels, layers);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -800,31 +819,31 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
case Format.Bc1RgbaSrgb:
|
case Format.Bc1RgbaSrgb:
|
||||||
case Format.Bc1RgbaUnorm:
|
case Format.Bc1RgbaUnorm:
|
||||||
result = BCnDecoder.DecodeBC1(result, width, height, depth, levels, layers);
|
result = BCnDecoder.DecodeBC1(result, width, height, sliceDepth, levels, layers);
|
||||||
break;
|
break;
|
||||||
case Format.Bc2Srgb:
|
case Format.Bc2Srgb:
|
||||||
case Format.Bc2Unorm:
|
case Format.Bc2Unorm:
|
||||||
result = BCnDecoder.DecodeBC2(result, width, height, depth, levels, layers);
|
result = BCnDecoder.DecodeBC2(result, width, height, sliceDepth, levels, layers);
|
||||||
break;
|
break;
|
||||||
case Format.Bc3Srgb:
|
case Format.Bc3Srgb:
|
||||||
case Format.Bc3Unorm:
|
case Format.Bc3Unorm:
|
||||||
result = BCnDecoder.DecodeBC3(result, width, height, depth, levels, layers);
|
result = BCnDecoder.DecodeBC3(result, width, height, sliceDepth, levels, layers);
|
||||||
break;
|
break;
|
||||||
case Format.Bc4Snorm:
|
case Format.Bc4Snorm:
|
||||||
case Format.Bc4Unorm:
|
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;
|
break;
|
||||||
case Format.Bc5Snorm:
|
case Format.Bc5Snorm:
|
||||||
case Format.Bc5Unorm:
|
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;
|
break;
|
||||||
case Format.Bc6HSfloat:
|
case Format.Bc6HSfloat:
|
||||||
case Format.Bc6HUfloat:
|
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;
|
break;
|
||||||
case Format.Bc7Srgb:
|
case Format.Bc7Srgb:
|
||||||
case Format.Bc7Unorm:
|
case Format.Bc7Unorm:
|
||||||
result = BCnDecoder.DecodeBC7(result, width, height, depth, levels, layers);
|
result = BCnDecoder.DecodeBC7(result, width, height, sliceDepth, levels, layers);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1484,11 +1503,12 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pool">The texture pool this texture has been added to</param>
|
/// <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>
|
/// <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)
|
lock (_poolOwners)
|
||||||
{
|
{
|
||||||
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id });
|
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id, GpuAddress = gpuVa });
|
||||||
}
|
}
|
||||||
_referenceCount++;
|
_referenceCount++;
|
||||||
|
|
||||||
@@ -1585,6 +1605,36 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
InvalidatedSequence++;
|
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>
|
/// <summary>
|
||||||
/// Delete the texture if it is not used anymore.
|
/// Delete the texture if it is not used anymore.
|
||||||
/// The texture is considered unused when the reference count is zero,
|
/// The texture is considered unused when the reference count is zero,
|
||||||
@@ -1636,7 +1686,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
Group.ClearModified(unmapRange);
|
Group.ClearModified(unmapRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveFromPools(true);
|
UpdatePoolMappings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@@ -194,6 +194,39 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_cache.Lift(texture);
|
_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>
|
/// <summary>
|
||||||
/// Tries to find an existing texture, or create a new one if not found.
|
/// Tries to find an existing texture, or create a new one if not found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@@ -39,6 +39,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
class TextureGroup : IDisposable
|
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);
|
private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,8 +121,30 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_allOffsets = size.AllOffsets;
|
_allOffsets = size.AllOffsets;
|
||||||
_sliceSizes = size.SliceSizes;
|
_sliceSizes = size.SliceSizes;
|
||||||
|
|
||||||
|
if (Storage.Target.HasDepthOrLayers() && Storage.Info.GetSlices() > GranularLayerThreshold)
|
||||||
|
{
|
||||||
|
_hasLayerViews = true;
|
||||||
|
_hasMipViews = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
(_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
|
(_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();
|
RecalculateHandleRegions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +276,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
bool anyModified = false;
|
bool anyModified = false;
|
||||||
bool anyUnmapped = false;
|
bool anyNotDirty = false;
|
||||||
|
|
||||||
for (int i = 0; i < regionCount; i++)
|
for (int i = 0; i < regionCount; i++)
|
||||||
{
|
{
|
||||||
@@ -294,20 +321,21 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
dirty |= handleDirty;
|
dirty |= handleDirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
anyUnmapped |= handleUnmapped;
|
|
||||||
|
|
||||||
if (group.NeedsCopy)
|
if (group.NeedsCopy)
|
||||||
{
|
{
|
||||||
// The texture we copied from is still being written to. Copy from it again the next time this texture is used.
|
// The texture we copied from is still being written to. Copy from it again the next time this texture is used.
|
||||||
texture.SignalGroupDirty();
|
texture.SignalGroupDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadNeeded[baseHandle + i] = handleDirty && !handleUnmapped;
|
bool loadNeeded = handleDirty && !handleUnmapped;
|
||||||
|
|
||||||
|
anyNotDirty |= !loadNeeded;
|
||||||
|
_loadNeeded[baseHandle + i] = loadNeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirty)
|
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.
|
// 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>
|
/// <param name="regionCount">The number of handles to synchronize</param>
|
||||||
private void SynchronizePartial(int baseHandle, int regionCount)
|
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++)
|
for (int i = 0; i < regionCount; i++)
|
||||||
{
|
{
|
||||||
if (_loadNeeded[baseHandle + i])
|
if (_loadNeeded[baseHandle + i])
|
||||||
{
|
{
|
||||||
var info = GetHandleInformation(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.
|
// 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 layer = 0; layer < info.Layers; layer++)
|
||||||
{
|
{
|
||||||
for (int level = 0; level < info.Levels; level++)
|
for (int level = 0; level < info.Levels; level++)
|
||||||
{
|
{
|
||||||
int offsetIndex = GetOffsetIndex(info.BaseLayer + layer, info.BaseLevel + level);
|
int offsetIndex = GetOffsetIndex(info.BaseLayer + layer, info.BaseLevel + level);
|
||||||
|
|
||||||
int offset = _allOffsets[offsetIndex];
|
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);
|
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>
|
/// <returns>A TextureGroupHandle covering the given views</returns>
|
||||||
private TextureGroupHandle GenerateHandles(int viewStart, int views)
|
private TextureGroupHandle GenerateHandles(int viewStart, int views)
|
||||||
{
|
{
|
||||||
|
int viewEnd = viewStart + views - 1;
|
||||||
|
(_, int lastLevel) = GetLayerLevelForView(viewEnd);
|
||||||
|
|
||||||
int offset = _allOffsets[viewStart];
|
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;
|
int size = endOffset - offset;
|
||||||
|
|
||||||
var result = new List<CpuRegionHandle>();
|
var result = new List<CpuRegionHandle>();
|
||||||
@@ -1057,19 +1120,61 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// The dirty flags from the previous handles will be kept.
|
/// The dirty flags from the previous handles will be kept.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="handles">The handles to replace the current handles with</param>
|
/// <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)
|
if (_handles != null)
|
||||||
{
|
{
|
||||||
// When replacing handles, they should start as non-dirty.
|
// When replacing handles, they should start as non-dirty.
|
||||||
|
|
||||||
foreach (TextureGroupHandle groupHandle in handles)
|
foreach (TextureGroupHandle groupHandle in handles)
|
||||||
|
{
|
||||||
|
if (rangeChanged)
|
||||||
|
{
|
||||||
|
// 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)
|
foreach (CpuRegionHandle handle in groupHandle.Handles)
|
||||||
{
|
{
|
||||||
handle.Reprotect();
|
handle.Reprotect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
InheritHandles(_handles, handles, 0);
|
InheritHandles(_handles, handles, 0);
|
||||||
|
|
||||||
@@ -1089,7 +1194,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recalculate handle regions for this texture group, and inherit existing state into the new handles.
|
/// Recalculate handle regions for this texture group, and inherit existing state into the new handles.
|
||||||
/// </summary>
|
/// </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;
|
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>
|
/// <summary>
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
|
using Ryujinx.Graphics.Gpu.Memory;
|
||||||
using Ryujinx.Graphics.Texture;
|
using Ryujinx.Graphics.Texture;
|
||||||
|
using Ryujinx.Memory.Range;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Gpu.Image
|
namespace Ryujinx.Graphics.Gpu.Image
|
||||||
{
|
{
|
||||||
@@ -12,8 +15,63 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool>
|
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 GpuChannel _channel;
|
||||||
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
|
private readonly ConcurrentQueue<DereferenceRequest> _dereferenceQueue = new ConcurrentQueue<DereferenceRequest>();
|
||||||
private TextureDescriptor _defaultDescriptor;
|
private TextureDescriptor _defaultDescriptor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -58,7 +116,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
TextureInfo info = GetInfo(descriptor, out int layerSize);
|
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);
|
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;
|
Items[id] = texture;
|
||||||
|
|
||||||
|
texture.IncrementReferenceCount(this, id, descriptor.UnpackAddress());
|
||||||
|
|
||||||
DescriptorCache[id] = descriptor;
|
DescriptorCache[id] = descriptor;
|
||||||
}
|
}
|
||||||
else
|
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>
|
/// <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)
|
public void ForceRemove(Texture texture, int id, bool deferred)
|
||||||
{
|
{
|
||||||
Items[id] = null;
|
var previous = Interlocked.Exchange(ref Items[id], null);
|
||||||
|
|
||||||
if (deferred)
|
if (deferred)
|
||||||
{
|
{
|
||||||
_dereferenceQueue.Enqueue(texture);
|
if (previous != null)
|
||||||
|
{
|
||||||
|
_dereferenceQueue.Enqueue(DereferenceRequest.Remove(texture));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
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>
|
/// <summary>
|
||||||
/// Process the dereference queue, decrementing the reference count for each texture in it.
|
/// 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.
|
/// This is used to ensure that texture disposal happens on the render thread.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
@@ -213,9 +353,10 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
_channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor);
|
_channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Interlocked.Exchange(ref Items[id], null) != null)
|
||||||
|
{
|
||||||
texture.DecrementReferenceCount(this, id);
|
texture.DecrementReferenceCount(this, id);
|
||||||
|
}
|
||||||
Items[id] = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -178,7 +178,7 @@ namespace Ryujinx.Graphics.OpenGL
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pipeline.Initialize(this);
|
_pipeline.Initialize(this);
|
||||||
_counters.Initialize();
|
_counters.Initialize(_pipeline);
|
||||||
|
|
||||||
// This is required to disable [0, 1] clamping for SNorm outputs on compatibility profiles.
|
// This is required to disable [0, 1] clamping for SNorm outputs on compatibility profiles.
|
||||||
// This call is expected to fail if we're running with a core profile,
|
// This call is expected to fail if we're running with a core profile,
|
||||||
|
@@ -773,6 +773,16 @@ namespace Ryujinx.Graphics.OpenGL
|
|||||||
_tfEnabled = false;
|
_tfEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double GetCounterDivisor(CounterType type)
|
||||||
|
{
|
||||||
|
if (type == CounterType.SamplesPassed)
|
||||||
|
{
|
||||||
|
return _renderScale[0].X * _renderScale[0].X;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
public void SetAlphaTest(bool enable, float reference, CompareOp op)
|
public void SetAlphaTest(bool enable, float reference, CompareOp op)
|
||||||
{
|
{
|
||||||
if (!enable)
|
if (!enable)
|
||||||
|
@@ -10,6 +10,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
{
|
{
|
||||||
private const int MaxQueryRetries = 5000;
|
private const int MaxQueryRetries = 5000;
|
||||||
private const long DefaultValue = -1;
|
private const long DefaultValue = -1;
|
||||||
|
private const ulong HighMask = 0xFFFFFFFF00000000;
|
||||||
|
|
||||||
public int Query { get; }
|
public int Query { get; }
|
||||||
|
|
||||||
@@ -63,11 +64,17 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool WaitingForValue(long data)
|
||||||
|
{
|
||||||
|
return data == DefaultValue ||
|
||||||
|
((ulong)data & HighMask) == (unchecked((ulong)DefaultValue) & HighMask);
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryGetResult(out long result)
|
public bool TryGetResult(out long result)
|
||||||
{
|
{
|
||||||
result = Marshal.ReadInt64(_bufferMap);
|
result = Marshal.ReadInt64(_bufferMap);
|
||||||
|
|
||||||
return result != DefaultValue;
|
return WaitingForValue(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long AwaitResult(AutoResetEvent wakeSignal = null)
|
public long AwaitResult(AutoResetEvent wakeSignal = null)
|
||||||
@@ -76,7 +83,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
|
|
||||||
if (wakeSignal == null)
|
if (wakeSignal == null)
|
||||||
{
|
{
|
||||||
while (data == DefaultValue)
|
while (WaitingForValue(data))
|
||||||
{
|
{
|
||||||
data = Marshal.ReadInt64(_bufferMap);
|
data = Marshal.ReadInt64(_bufferMap);
|
||||||
}
|
}
|
||||||
@@ -84,10 +91,10 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
while (data == DefaultValue && iterations++ < MaxQueryRetries)
|
while (WaitingForValue(data) && iterations++ < MaxQueryRetries)
|
||||||
{
|
{
|
||||||
data = Marshal.ReadInt64(_bufferMap);
|
data = Marshal.ReadInt64(_bufferMap);
|
||||||
if (data == DefaultValue)
|
if (WaitingForValue(data))
|
||||||
{
|
{
|
||||||
wakeSignal.WaitOne(1);
|
wakeSignal.WaitOne(1);
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,8 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
public CounterType Type { get; }
|
public CounterType Type { get; }
|
||||||
public bool Disposed { get; private set; }
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
|
private readonly Pipeline _pipeline;
|
||||||
|
|
||||||
private Queue<CounterQueueEvent> _events = new Queue<CounterQueueEvent>();
|
private Queue<CounterQueueEvent> _events = new Queue<CounterQueueEvent>();
|
||||||
private CounterQueueEvent _current;
|
private CounterQueueEvent _current;
|
||||||
|
|
||||||
@@ -28,10 +30,12 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
|
|
||||||
private Thread _consumerThread;
|
private Thread _consumerThread;
|
||||||
|
|
||||||
internal CounterQueue(CounterType type)
|
internal CounterQueue(Pipeline pipeline, CounterType type)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
|
|
||||||
|
_pipeline = pipeline;
|
||||||
|
|
||||||
QueryTarget glType = GetTarget(Type);
|
QueryTarget glType = GetTarget(Type);
|
||||||
|
|
||||||
_queryPool = new Queue<BufferedQuery>(QueryPoolInitialSize);
|
_queryPool = new Queue<BufferedQuery>(QueryPoolInitialSize);
|
||||||
@@ -119,7 +123,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
_current.ReserveForHostAccess();
|
_current.ReserveForHostAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
_current.Complete(draws > 0);
|
_current.Complete(draws > 0, _pipeline.GetCounterDivisor(Type));
|
||||||
_events.Enqueue(_current);
|
_events.Enqueue(_current);
|
||||||
|
|
||||||
_current.OnResult += resultHandler;
|
_current.OnResult += resultHandler;
|
||||||
|
@@ -26,6 +26,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
|
|
||||||
private object _lock = new object();
|
private object _lock = new object();
|
||||||
private ulong _result = ulong.MaxValue;
|
private ulong _result = ulong.MaxValue;
|
||||||
|
private double _divisor = 1f;
|
||||||
|
|
||||||
public CounterQueueEvent(CounterQueue queue, QueryTarget type, ulong drawIndex)
|
public CounterQueueEvent(CounterQueue queue, QueryTarget type, ulong drawIndex)
|
||||||
{
|
{
|
||||||
@@ -45,9 +46,11 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
ClearCounter = true;
|
ClearCounter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Complete(bool withResult)
|
internal void Complete(bool withResult, double divisor)
|
||||||
{
|
{
|
||||||
_counter.End(withResult);
|
_counter.End(withResult);
|
||||||
|
|
||||||
|
_divisor = divisor;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool TryConsume(ref ulong result, bool block, AutoResetEvent wakeSignal = null)
|
internal bool TryConsume(ref ulong result, bool block, AutoResetEvent wakeSignal = null)
|
||||||
@@ -78,7 +81,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result += (ulong)queryResult;
|
result += _divisor == 1 ? (ulong)queryResult : (ulong)Math.Ceiling(queryResult / _divisor);
|
||||||
|
|
||||||
_result = result;
|
_result = result;
|
||||||
|
|
||||||
|
@@ -14,12 +14,12 @@ namespace Ryujinx.Graphics.OpenGL.Queries
|
|||||||
_counterQueues = new CounterQueue[count];
|
_counterQueues = new CounterQueue[count];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize(Pipeline pipeline)
|
||||||
{
|
{
|
||||||
for (int index = 0; index < _counterQueues.Length; index++)
|
for (int index = 0; index < _counterQueues.Length; index++)
|
||||||
{
|
{
|
||||||
CounterType type = (CounterType)index;
|
CounterType type = (CounterType)index;
|
||||||
_counterQueues[index] = new CounterQueue(type);
|
_counterQueues[index] = new CounterQueue(pipeline, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -112,7 +112,7 @@ namespace Ryujinx.Graphics.Texture
|
|||||||
int outSize = GetTextureSize(
|
int outSize = GetTextureSize(
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
depth,
|
sliceDepth,
|
||||||
levels,
|
levels,
|
||||||
layers,
|
layers,
|
||||||
blockWidth,
|
blockWidth,
|
||||||
|
@@ -684,6 +684,16 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
_tfEnabled = false;
|
_tfEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double GetCounterDivisor(CounterType type)
|
||||||
|
{
|
||||||
|
if (type == CounterType.SamplesPassed)
|
||||||
|
{
|
||||||
|
return _renderScale[0].X * _renderScale[0].X;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsCommandBufferActive(CommandBuffer cb)
|
public bool IsCommandBufferActive(CommandBuffer cb)
|
||||||
{
|
{
|
||||||
return CommandBuffer.Handle == cb.Handle;
|
return CommandBuffer.Handle == cb.Handle;
|
||||||
|
@@ -12,6 +12,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
private const int MaxQueryRetries = 5000;
|
private const int MaxQueryRetries = 5000;
|
||||||
private const long DefaultValue = -1;
|
private const long DefaultValue = -1;
|
||||||
private const long DefaultValueInt = 0xFFFFFFFF;
|
private const long DefaultValueInt = 0xFFFFFFFF;
|
||||||
|
private const ulong HighMask = 0xFFFFFFFF00000000;
|
||||||
|
|
||||||
private readonly Vk _api;
|
private readonly Vk _api;
|
||||||
private readonly Device _device;
|
private readonly Device _device;
|
||||||
@@ -125,6 +126,12 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool WaitingForValue(long data)
|
||||||
|
{
|
||||||
|
return data == _defaultValue ||
|
||||||
|
(!_result32Bit && ((ulong)data & HighMask) == ((ulong)_defaultValue & HighMask));
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryGetResult(out long result)
|
public bool TryGetResult(out long result)
|
||||||
{
|
{
|
||||||
result = Marshal.ReadInt64(_bufferMap);
|
result = Marshal.ReadInt64(_bufferMap);
|
||||||
@@ -138,7 +145,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
|
|
||||||
if (wakeSignal == null)
|
if (wakeSignal == null)
|
||||||
{
|
{
|
||||||
while (data == _defaultValue)
|
while (WaitingForValue(data))
|
||||||
{
|
{
|
||||||
data = Marshal.ReadInt64(_bufferMap);
|
data = Marshal.ReadInt64(_bufferMap);
|
||||||
}
|
}
|
||||||
@@ -146,10 +153,10 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
while (data == _defaultValue && iterations++ < MaxQueryRetries)
|
while (WaitingForValue(data) && iterations++ < MaxQueryRetries)
|
||||||
{
|
{
|
||||||
data = Marshal.ReadInt64(_bufferMap);
|
data = Marshal.ReadInt64(_bufferMap);
|
||||||
if (data == _defaultValue)
|
if (WaitingForValue(data))
|
||||||
{
|
{
|
||||||
wakeSignal.WaitOne(1);
|
wakeSignal.WaitOne(1);
|
||||||
}
|
}
|
||||||
|
@@ -148,7 +148,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
_current.ReserveForHostAccess();
|
_current.ReserveForHostAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
_current.Complete(draws > 0 && Type != CounterType.TransformFeedbackPrimitivesWritten);
|
_current.Complete(draws > 0 && Type != CounterType.TransformFeedbackPrimitivesWritten, _pipeline.GetCounterDivisor(Type));
|
||||||
_events.Enqueue(_current);
|
_events.Enqueue(_current);
|
||||||
|
|
||||||
_current.OnResult += resultHandler;
|
_current.OnResult += resultHandler;
|
||||||
|
@@ -24,6 +24,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
|
|
||||||
private object _lock = new object();
|
private object _lock = new object();
|
||||||
private ulong _result = ulong.MaxValue;
|
private ulong _result = ulong.MaxValue;
|
||||||
|
private double _divisor = 1f;
|
||||||
|
|
||||||
public CounterQueueEvent(CounterQueue queue, CounterType type, ulong drawIndex)
|
public CounterQueueEvent(CounterQueue queue, CounterType type, ulong drawIndex)
|
||||||
{
|
{
|
||||||
@@ -52,9 +53,11 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
ClearCounter = true;
|
ClearCounter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Complete(bool withResult)
|
internal void Complete(bool withResult, double divisor)
|
||||||
{
|
{
|
||||||
_counter.End(withResult);
|
_counter.End(withResult);
|
||||||
|
|
||||||
|
_divisor = divisor;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool TryConsume(ref ulong result, bool block, AutoResetEvent wakeSignal = null)
|
internal bool TryConsume(ref ulong result, bool block, AutoResetEvent wakeSignal = null)
|
||||||
@@ -85,7 +88,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result += (ulong)queryResult;
|
result += _divisor == 1 ? (ulong)queryResult : (ulong)Math.Ceiling(queryResult / _divisor);
|
||||||
|
|
||||||
_result = result;
|
_result = result;
|
||||||
|
|
||||||
|
@@ -8,6 +8,8 @@ namespace Ryujinx.Memory.Range
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct MultiRange : IEquatable<MultiRange>
|
public readonly struct MultiRange : IEquatable<MultiRange>
|
||||||
{
|
{
|
||||||
|
private const ulong InvalidAddress = ulong.MaxValue;
|
||||||
|
|
||||||
private readonly MemoryRange _singleRange;
|
private readonly MemoryRange _singleRange;
|
||||||
private readonly MemoryRange[] _ranges;
|
private readonly MemoryRange[] _ranges;
|
||||||
|
|
||||||
@@ -107,7 +109,16 @@ namespace Ryujinx.Memory.Range
|
|||||||
else if (offset < range.Size)
|
else if (offset < range.Size)
|
||||||
{
|
{
|
||||||
ulong sliceSize = Math.Min(size, range.Size - offset);
|
ulong sliceSize = Math.Min(size, range.Size - offset);
|
||||||
|
|
||||||
|
if (range.Address == InvalidAddress)
|
||||||
|
{
|
||||||
|
ranges.Add(new MemoryRange(range.Address, sliceSize));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
ranges.Add(new MemoryRange(range.Address + offset, sliceSize));
|
ranges.Add(new MemoryRange(range.Address + offset, sliceSize));
|
||||||
|
}
|
||||||
|
|
||||||
size -= sliceSize;
|
size -= sliceSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,31 +6,31 @@ PUBLISH_DIRECTORY=$1
|
|||||||
OUTPUT_DIRECTORY=$2
|
OUTPUT_DIRECTORY=$2
|
||||||
ENTITLEMENTS_FILE_PATH=$3
|
ENTITLEMENTS_FILE_PATH=$3
|
||||||
|
|
||||||
APP_BUNDLE_DIRECTORY=$OUTPUT_DIRECTORY/Ryujinx.app
|
APP_BUNDLE_DIRECTORY="$OUTPUT_DIRECTORY/Ryujinx.app"
|
||||||
|
|
||||||
rm -rf $APP_BUNDLE_DIRECTORY
|
rm -rf "$APP_BUNDLE_DIRECTORY"
|
||||||
mkdir -p $APP_BUNDLE_DIRECTORY/Contents
|
mkdir -p "$APP_BUNDLE_DIRECTORY/Contents"
|
||||||
mkdir $APP_BUNDLE_DIRECTORY/Contents/Frameworks
|
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
|
||||||
mkdir $APP_BUNDLE_DIRECTORY/Contents/MacOS
|
mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
|
||||||
mkdir $APP_BUNDLE_DIRECTORY/Contents/Resources
|
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
|
||||||
|
|
||||||
# Copy executables first
|
# Copy executables first
|
||||||
cp $PUBLISH_DIRECTORY/Ryujinx.Ava $APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx
|
cp "$PUBLISH_DIRECTORY/Ryujinx.Ava" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
|
||||||
chmod u+x $APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx
|
chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
|
||||||
|
|
||||||
# Then all libraries
|
# Then all libraries
|
||||||
cp $PUBLISH_DIRECTORY/*.dylib $APP_BUNDLE_DIRECTORY/Contents/Frameworks
|
cp "$PUBLISH_DIRECTORY"/*.dylib "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
|
||||||
|
|
||||||
# Then resources
|
# Then resources
|
||||||
cp Info.plist $APP_BUNDLE_DIRECTORY/Contents
|
cp Info.plist "$APP_BUNDLE_DIRECTORY/Contents"
|
||||||
cp Ryujinx.icns $APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns
|
cp Ryujinx.icns "$APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns"
|
||||||
cp updater.sh $APP_BUNDLE_DIRECTORY/Contents/Resources/updater.sh
|
cp updater.sh "$APP_BUNDLE_DIRECTORY/Contents/Resources/updater.sh"
|
||||||
cp -r $PUBLISH_DIRECTORY/THIRDPARTY.md $APP_BUNDLE_DIRECTORY/Contents/Resources
|
cp -r "$PUBLISH_DIRECTORY/THIRDPARTY.md" "$APP_BUNDLE_DIRECTORY/Contents/Resources"
|
||||||
|
|
||||||
echo -n "APPL????" > $APP_BUNDLE_DIRECTORY/Contents/PkgInfo
|
echo -n "APPL????" > "$APP_BUNDLE_DIRECTORY/Contents/PkgInfo"
|
||||||
|
|
||||||
# Fixup libraries and executable
|
# Fixup libraries and executable
|
||||||
python3 bundle_fix_up.py $APP_BUNDLE_DIRECTORY MacOS/Ryujinx
|
python3 bundle_fix_up.py "$APP_BUNDLE_DIRECTORY" MacOS/Ryujinx
|
||||||
|
|
||||||
# Now sign it
|
# Now sign it
|
||||||
if ! [ -x "$(command -v codesign)" ];
|
if ! [ -x "$(command -v codesign)" ];
|
||||||
@@ -44,9 +44,9 @@ then
|
|||||||
# NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
|
# NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
|
||||||
# cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
|
# cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
|
||||||
echo "Usign rcodesign for ad-hoc signing"
|
echo "Usign rcodesign for ad-hoc signing"
|
||||||
rcodesign sign --entitlements-xml-path $ENTITLEMENTS_FILE_PATH $APP_BUNDLE_DIRECTORY
|
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY"
|
||||||
else
|
else
|
||||||
echo "Usign codesign for ad-hoc signing"
|
echo "Usign codesign for ad-hoc signing"
|
||||||
codesign --entitlements $ENTITLEMENTS_FILE_PATH -f --deep -s - $APP_BUNDLE_DIRECTORY
|
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$APP_BUNDLE_DIRECTORY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@@ -7,54 +7,54 @@ if [ "$#" -ne 6 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p $1
|
mkdir -p "$1"
|
||||||
mkdir -p $2
|
mkdir -p "$2"
|
||||||
mkdir -p $3
|
mkdir -p "$3"
|
||||||
|
|
||||||
BASE_DIR=$(readlink -f $1)
|
BASE_DIR=$(readlink -f "$1")
|
||||||
TEMP_DIRECTORY=$(readlink -f $2)
|
TEMP_DIRECTORY=$(readlink -f "$2")
|
||||||
OUTPUT_DIRECTORY=$(readlink -f $3)
|
OUTPUT_DIRECTORY=$(readlink -f "$3")
|
||||||
ENTITLEMENTS_FILE_PATH=$(readlink -f $4)
|
ENTITLEMENTS_FILE_PATH=$(readlink -f "$4")
|
||||||
VERSION=$5
|
VERSION=$5
|
||||||
SOURCE_REVISION_ID=$6
|
SOURCE_REVISION_ID=$6
|
||||||
|
|
||||||
RELEASE_TAR_FILE_NAME=Ryujinx-$VERSION-macos_universal.app.tar
|
RELEASE_TAR_FILE_NAME=Ryujinx-$VERSION-macos_universal.app.tar
|
||||||
ARM64_APP_BUNDLE=$TEMP_DIRECTORY/output_arm64/Ryujinx.app
|
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
|
||||||
X64_APP_BUNDLE=$TEMP_DIRECTORY/output_x64/Ryujinx.app
|
X64_APP_BUNDLE="$TEMP_DIRECTORY/output_x64/Ryujinx.app"
|
||||||
UNIVERSAL_APP_BUNDLE=$OUTPUT_DIRECTORY/Ryujinx.app
|
UNIVERSAL_APP_BUNDLE="$OUTPUT_DIRECTORY/Ryujinx.app"
|
||||||
EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
|
EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
|
||||||
|
|
||||||
rm -rf $TEMP_DIRECTORY
|
rm -rf "$TEMP_DIRECTORY"
|
||||||
mkdir -p $TEMP_DIRECTORY
|
mkdir -p "$TEMP_DIRECTORY"
|
||||||
|
|
||||||
DOTNET_COMMON_ARGS="-p:DebugType=embedded -p:Version=$VERSION -p:SourceRevisionId=$SOURCE_REVISION_ID --self-contained true"
|
DOTNET_COMMON_ARGS="-p:DebugType=embedded -p:Version=$VERSION -p:SourceRevisionId=$SOURCE_REVISION_ID --self-contained true"
|
||||||
|
|
||||||
dotnet restore
|
dotnet restore
|
||||||
dotnet build -c Release Ryujinx.Ava
|
dotnet build -c Release Ryujinx.Ava
|
||||||
dotnet publish -c Release -r osx-arm64 -o $TEMP_DIRECTORY/publish_arm64 $DOTNET_COMMON_ARGS Ryujinx.Ava
|
dotnet publish -c Release -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" $DOTNET_COMMON_ARGS Ryujinx.Ava
|
||||||
dotnet publish -c Release -r osx-x64 -o $TEMP_DIRECTORY/publish_x64 $DOTNET_COMMON_ARGS Ryujinx.Ava
|
dotnet publish -c Release -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" $DOTNET_COMMON_ARGS Ryujinx.Ava
|
||||||
|
|
||||||
# Get ride of the support library for ARMeilleur for x64 (that's only for arm64)
|
# Get ride of the support library for ARMeilleur for x64 (that's only for arm64)
|
||||||
rm -rf $TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib
|
rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
|
||||||
|
|
||||||
# Get ride of libsoundio from arm64 builds as we don't have a arm64 variant
|
# Get ride of libsoundio from arm64 builds as we don't have a arm64 variant
|
||||||
# TODO: remove this once done
|
# TODO: remove this once done
|
||||||
rm -rf $TEMP_DIRECTORY/publish_arm64/libsoundio.dylib
|
rm -rf "$TEMP_DIRECTORY/publish_arm64/libsoundio.dylib"
|
||||||
|
|
||||||
pushd $BASE_DIR/distribution/macos
|
pushd "$BASE_DIR/distribution/macos"
|
||||||
./create_app_bundle.sh $TEMP_DIRECTORY/publish_x64 $TEMP_DIRECTORY/output_x64 $ENTITLEMENTS_FILE_PATH
|
./create_app_bundle.sh "$TEMP_DIRECTORY/publish_x64" "$TEMP_DIRECTORY/output_x64" "$ENTITLEMENTS_FILE_PATH"
|
||||||
./create_app_bundle.sh $TEMP_DIRECTORY/publish_arm64 $TEMP_DIRECTORY/output_arm64 $ENTITLEMENTS_FILE_PATH
|
./create_app_bundle.sh "$TEMP_DIRECTORY/publish_arm64" "$TEMP_DIRECTORY/output_arm64" "$ENTITLEMENTS_FILE_PATH"
|
||||||
popd
|
popd
|
||||||
|
|
||||||
rm -rf $UNIVERSAL_APP_BUNDLE
|
rm -rf "$UNIVERSAL_APP_BUNDLE"
|
||||||
mkdir -p $OUTPUT_DIRECTORY
|
mkdir -p "$OUTPUT_DIRECTORY"
|
||||||
|
|
||||||
# Let's copy one of the two different app bundle and remove the executable
|
# Let's copy one of the two different app bundle and remove the executable
|
||||||
cp -R $ARM64_APP_BUNDLE $UNIVERSAL_APP_BUNDLE
|
cp -R "$ARM64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE"
|
||||||
rm $UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH
|
rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH"
|
||||||
|
|
||||||
# Make it libraries universal
|
# Make it libraries universal
|
||||||
python3 $BASE_DIR/distribution/macos/construct_universal_dylib.py $ARM64_APP_BUNDLE $X64_APP_BUNDLE $UNIVERSAL_APP_BUNDLE "**/*.dylib"
|
python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_BUNDLE" "$X64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" "**/*.dylib"
|
||||||
|
|
||||||
if ! [ -x "$(command -v lipo)" ];
|
if ! [ -x "$(command -v lipo)" ];
|
||||||
then
|
then
|
||||||
@@ -69,12 +69,12 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Make it the executable universal
|
# Make it the executable universal
|
||||||
$LIPO $ARM64_APP_BUNDLE/$EXECUTABLE_SUB_PATH $X64_APP_BUNDLE/$EXECUTABLE_SUB_PATH -output $UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH -create
|
$LIPO "$ARM64_APP_BUNDLE/$EXECUTABLE_SUB_PATH" "$X64_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -output "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -create
|
||||||
|
|
||||||
# Patch up the Info.plist to have appropriate version
|
# Patch up the Info.plist to have appropriate version
|
||||||
sed -r -i.bck "s/\%\%RYUJINX_BUILD_VERSION\%\%/$VERSION/g;" $UNIVERSAL_APP_BUNDLE/Contents/Info.plist
|
sed -r -i.bck "s/\%\%RYUJINX_BUILD_VERSION\%\%/$VERSION/g;" "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist"
|
||||||
sed -r -i.bck "s/\%\%RYUJINX_BUILD_GIT_HASH\%\%/$SOURCE_REVISION_ID/g;" $UNIVERSAL_APP_BUNDLE/Contents/Info.plist
|
sed -r -i.bck "s/\%\%RYUJINX_BUILD_GIT_HASH\%\%/$SOURCE_REVISION_ID/g;" "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist"
|
||||||
rm $UNIVERSAL_APP_BUNDLE/Contents/Info.plist.bck
|
rm "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist.bck"
|
||||||
|
|
||||||
# Now sign it
|
# Now sign it
|
||||||
if ! [ -x "$(command -v codesign)" ];
|
if ! [ -x "$(command -v codesign)" ];
|
||||||
@@ -88,16 +88,16 @@ then
|
|||||||
# NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
|
# NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
|
||||||
# cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
|
# cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
|
||||||
echo "Usign rcodesign for ad-hoc signing"
|
echo "Usign rcodesign for ad-hoc signing"
|
||||||
rcodesign sign --entitlements-xml-path $ENTITLEMENTS_FILE_PATH $UNIVERSAL_APP_BUNDLE
|
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE"
|
||||||
else
|
else
|
||||||
echo "Usign codesign for ad-hoc signing"
|
echo "Usign codesign for ad-hoc signing"
|
||||||
codesign --entitlements $ENTITLEMENTS_FILE_PATH -f --deep -s - $UNIVERSAL_APP_BUNDLE
|
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$UNIVERSAL_APP_BUNDLE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Creating archive"
|
echo "Creating archive"
|
||||||
pushd $OUTPUT_DIRECTORY
|
pushd "$OUTPUT_DIRECTORY"
|
||||||
tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf $RELEASE_TAR_FILE_NAME Ryujinx.app 1> /dev/null
|
tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf $RELEASE_TAR_FILE_NAME Ryujinx.app 1> /dev/null
|
||||||
python3 $BASE_DIR/distribution/misc/add_tar_exec.py $RELEASE_TAR_FILE_NAME "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
|
python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" $RELEASE_TAR_FILE_NAME "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
|
||||||
gzip -9 < $RELEASE_TAR_FILE_NAME > $RELEASE_TAR_FILE_NAME.gz
|
gzip -9 < $RELEASE_TAR_FILE_NAME > $RELEASE_TAR_FILE_NAME.gz
|
||||||
rm $RELEASE_TAR_FILE_NAME
|
rm $RELEASE_TAR_FILE_NAME
|
||||||
popd
|
popd
|
||||||
|
Reference in New Issue
Block a user