Compare commits

..

7 Commits

Author SHA1 Message Date
riperiperi
1fc90e57d2 Update range for remapped sparse textures instead of recreating them (#4442)
* Update sparsely mapped texture ranges without recreating

Important TODO in TexturePool. Smaller TODO: should I look into making textures with views also do this? It needs to be able to detect if the views can be instantly deleted without issue if they're now remapped.

* Actually do partial updates

* Signal group dirty after mappings changed

* Fix various issues (should work now)

* Further optimisation

Should load a lot less data (16x) when partial updating 3d textures.

* Improve stability

* Allow granular uploads on large textures, improve rules

* Actually avoid updating slices that aren't modified.

* Address some feedback, minor optimisation

* Small tweak

* Refactor DereferenceRequest

More specific initialization methods.

* Improve code for resetting handles

* Explain data loading a bit more

* Add some safety for setting null from different threads.

All texture sets come from the one thread, but null sets can come from multiple. Only decrement ref count if we succeeded the null set first.

* Address feedback 1

* Make a bit safer
2023-03-14 17:08:44 -03:00
Isaac Marovitz
eafcc314a9 Ava UI: DownloadableContentManager Refactor (#4300)
* Start refactor

* Move around functions

* It builds

* Menu opens

* Buttons

* Fix overlapping text

* SaveAndClose and Close buttons

* Remove button

* Layout

* It’s a little funky but it works

* Enable all/disable all buttons

* Fix UpdateCount desyncs

* Search bar

* Search by title id

* Fix fuck ups

* Fix selection mode

* Update Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs

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

* Update Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs

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

* Update Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs

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

* Fix search bar

* Log corrupted DLC json

* Fix LibHac changes

---------

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-03-14 17:04:38 +01:00
riperiperi
6e9bd4de13 GPU: Scale counter results before addition (#4471)
* GPU: Scale counter results before addition

Counter results were being scaled on ReportCounter, which meant that the _total_ value of the counter was being scaled. Not only could this result in very large numbers and weird overflows if the game doesn't clear the counter, but it also caused the result to change drastically.

This PR changes scaling to be done when the value is added to the counter on the backend. This should evaluate the scale at the same time as before, on report counter, but avoiding the issue with scaling the total.

Fixes scaling in Warioware, at least in the demo, where it seems to compare old/new counters and broke down when scaling was enabled.

* Fix issues when result is partially uploaded.

Drivers tend to write the low half first, then the high half. Retry if the high half is FFFFFFFF.
2023-03-12 18:01:15 +01:00
TimeZlicer
05a41b31bc Misc: Support space in path on macOS distribution (#4462)
* .

* Apply suggestions from code review

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* wildcard(*) needs to be outside of quotes(") for cp to work

---------

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
2023-03-12 17:21:21 +01:00
MutantAura
eed17f963e Increase access permissions for Ava timezones (#4538) 2023-03-12 17:20:09 +01:00
TSRBerry
c09c0c002d [Flatpak] Beautify multiline strings again & Add full git commit hash (#4535)
* Don't destroy multiline strings

* Use full git commit hash
2023-03-12 10:42:33 +01:00
Mary
d56d335c0b misc: Some dependencies cleanup (#4507)
* Remove dependencies on libraries provided by .NET standard library

* Use System.IO.Hashing instead of Crc32.NET
2023-03-12 03:24:11 +01:00
35 changed files with 1120 additions and 547 deletions

View File

@@ -35,7 +35,7 @@ jobs:
id: version_info
working-directory: Ryujinx
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
with:
@@ -84,7 +84,7 @@ jobs:
- name: Update flatpak metadata
id: metadata
env:
RYUJINX_GIT_HASH: ${{ steps.version_info.outputs.git_short_hash }}
RYUJINX_GIT_HASH: ${{ steps.version_info.outputs.git_hash }}
shell: python
run: |
import hashlib
@@ -94,7 +94,17 @@ jobs:
import yaml
from datetime import datetime
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"
xml_file = "flathub/org.ryujinx.Ryujinx.appdata.xml"

View File

@@ -12,7 +12,6 @@
<PackageVersion Include="Avalonia.Svg.Skia" Version="0.10.18" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Concentus" Version="1.1.7" />
<PackageVersion Include="Crc32.NET" Version="1.2.0" />
<PackageVersion Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageVersion Include="DynamicData" Version="7.12.11" />
<PackageVersion Include="FluentAvaloniaUI" Version="1.4.5" />
@@ -45,10 +44,8 @@
<PackageVersion Include="SPB" Version="0.0.4-build28" />
<PackageVersion Include="System.Drawing.Common" Version="7.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.27.0" />
<PackageVersion Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageVersion Include="System.IO.Hashing" Version="7.0.0" />
<PackageVersion Include="System.Management" Version="7.0.0" />
<PackageVersion Include="System.Net.NameResolution" Version="4.3.0" />
<PackageVersion Include="System.Threading.ThreadPool" Version="4.3.0" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-f7c841d" />
<PackageVersion Include="XamlNameReferenceGenerator" Version="1.5.1" />
</ItemGroup>

View File

@@ -583,10 +583,10 @@
"SelectUpdateDialogTitle": "Select update files",
"UserProfileWindowTitle": "User Profiles Manager",
"CheatWindowTitle": "Cheats Manager",
"DlcWindowTitle": "Downloadable Content Manager",
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
"UpdateWindowTitle": "Title Update Manager",
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
"DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
"DlcWindowHeading": "{0} Downloadable Content(s)",
"UserProfilesEditProfile": "Edit Selected",
"Cancel": "Cancel",
"Save": "Save",

View File

@@ -13,7 +13,7 @@
</PropertyGroup>
<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>
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">

View File

@@ -1,4 +1,5 @@
using Ryujinx.Ava.UI.ViewModels;
using System.IO;
namespace Ryujinx.Ava.UI.Models
{
@@ -21,6 +22,8 @@ namespace Ryujinx.Ava.UI.Models
public string ContainerPath { get; }
public string FullPath { get; }
public string FileName => Path.GetFileName(ContainerPath);
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
{
TitleId = titleId;

View 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)));
}
}
}
}

View File

@@ -1564,7 +1564,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
if (SelectedApplication != null)
{
await new DownloadableContentManagerWindow(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName).ShowDialog(TopLevel as Window);
await DownloadableContentManagerWindow.Show(VirtualFileSystem, ulong.Parse(SelectedApplication.TitleId, NumberStyles.HexNumber), SelectedApplication.TitleName);
}
}

View File

@@ -236,7 +236,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public DateTimeOffset DateOffset { 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 ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }

View File

@@ -1,172 +1,194 @@
<window:StyleableWindow
<UserControl
x:Class="Ryujinx.Ava.UI.Windows.DownloadableContentManagerWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
Width="800"
Height="500"
MinWidth="800"
MinHeight="500"
MaxWidth="800"
MaxHeight="500"
SizeToContent="Height"
WindowStartupLocation="CenterOwner"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
Width="500"
Height="380"
mc:Ignorable="d"
x:CompileBindings="True"
x:DataType="viewModels:DownloadableContentManagerViewModel"
Focusable="True">
<Grid Name="DownloadableContentGrid" Margin="15">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Name="Heading"
Grid.Row="1"
MaxWidth="500"
Margin="20,15,20,20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
LineHeight="18"
TextAlignment="Center"
TextWrapping="Wrap" />
<DockPanel
Grid.Row="2"
Margin="0"
HorizontalAlignment="Left">
<Button
Name="EnableAllButton"
MinWidth="90"
Margin="5"
Command="{Binding EnableAll}">
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
</Button>
<Button
Name="DisableAllButton"
MinWidth="90"
Margin="5"
Command="{Binding DisableAll}">
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
</Button>
</DockPanel>
<Panel
Margin="0 0 0 10"
Grid.Row="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="{Binding UpdateCount}" />
<StackPanel
Margin="10 0"
Grid.Column="1"
Orientation="Horizontal">
<Button
Name="EnableAllButton"
MinWidth="90"
Margin="5"
Command="{ReflectionBinding EnableAll}">
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
</Button>
<Button
Name="DisableAllButton"
MinWidth="90"
Margin="5"
Command="{ReflectionBinding DisableAll}">
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
</Button>
</StackPanel>
<TextBox
Grid.Column="2"
MinHeight="27"
MaxHeight="27"
HorizontalAlignment="Stretch"
Watermark="{locale:Locale Search}"
Text="{Binding Search}" />
</Grid>
</Panel>
<Border
Grid.Row="3"
Margin="5"
Grid.Row="1"
Margin="0 0 0 24"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Gray"
BorderThickness="1">
<ScrollViewer
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<DataGrid
Name="DlcDataGrid"
MinHeight="200"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="True"
HorizontalScrollBarVisibility="Auto"
Items="{Binding _downloadableContents}"
SelectionMode="Extended"
VerticalScrollBarVisibility="Auto">
<DataGrid.Styles>
<Styles>
<Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
</Style>
</Styles>
<Styles>
<Style Selector="DataGridCell:nth-child(1)">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
</Style>
</Styles>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTemplateColumn Width="90">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox
Width="50"
MinWidth="40"
HorizontalAlignment="Center"
IsChecked="{Binding Enabled}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
</DataGridTemplateColumn.Header>
</DataGridTemplateColumn>
<DataGridTextColumn Width="140" Binding="{Binding TitleId}">
<DataGridTextColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Width="280" Binding="{Binding FullPath}">
<DataGridTextColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding ContainerPath}">
<DataGridTextColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1"
CornerRadius="5"
Padding="2.5">
<ListBox
AutoScrollToSelectedItem="False"
VirtualizationMode="None"
SelectionMode="Multiple, Toggle"
Background="Transparent"
SelectionChanged="OnSelectionChanged"
SelectedItems="{Binding SelectedDownloadableContents, Mode=TwoWay}"
Items="{Binding Views}">
<ListBox.DataTemplates>
<DataTemplate
DataType="models:DownloadableContentModel">
<Panel Margin="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
MaxLines="2"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
Text="{Binding FileName}" />
<TextBlock
Grid.Column="1"
Margin="10 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TitleId}" />
</Grid>
<StackPanel
Grid.Column="1"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Click="OpenLocation">
<ui:SymbolIcon
Symbol="OpenFolder"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
<Button
VerticalAlignment="Center"
HorizontalAlignment="Right"
Padding="10"
MinWidth="0"
MinHeight="0"
Click="RemoveDLC">
<ui:SymbolIcon
Symbol="Cancel"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Button>
</StackPanel>
</Grid>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
</Style>
</ListBox.Styles>
</ListBox>
</Border>
<DockPanel
Grid.Row="4"
Margin="0"
<Panel
Grid.Row="2"
HorizontalAlignment="Stretch">
<DockPanel Margin="0" HorizontalAlignment="Left">
<StackPanel
Orientation="Horizontal"
Spacing="10"
HorizontalAlignment="Left">
<Button
Name="AddButton"
MinWidth="90"
Margin="5"
Command="{Binding Add}">
Command="{ReflectionBinding Add}">
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
</Button>
<Button
Name="RemoveButton"
MinWidth="90"
Margin="5"
Command="{Binding RemoveSelected}">
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
</Button>
<Button
Name="RemoveAllButton"
MinWidth="90"
Margin="5"
Command="{Binding RemoveAll}">
Command="{ReflectionBinding RemoveAll}">
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
</Button>
</DockPanel>
<DockPanel Margin="0" HorizontalAlignment="Right">
</StackPanel>
<StackPanel
Orientation="Horizontal"
Spacing="10"
HorizontalAlignment="Right">
<Button
Name="SaveButton"
MinWidth="90"
Margin="5"
Command="{Binding SaveAndClose}">
Click="SaveAndClose">
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
</Button>
<Button
Name="CancelButton"
MinWidth="90"
Margin="5"
Command="{Binding Close}">
Click="Close">
<TextBlock Text="{locale:Locale InputDialogCancel}" />
</Button>
</DockPanel>
</DockPanel>
</StackPanel>
</Panel>
</Grid>
</window:StyleableWindow>
</UserControl>

View File

@@ -1,314 +1,115 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using Ryujinx.Ui.Common.Helper;
using System.Threading.Tasks;
using Path = System.IO.Path;
using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows
{
public partial class DownloadableContentManagerWindow : StyleableWindow
public partial class DownloadableContentManagerWindow : UserControl
{
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
private readonly string _downloadableContentJsonPath;
private VirtualFileSystem _virtualFileSystem { get; }
private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; }
private ulong _titleId { get; }
private string _titleName { get; }
public DownloadableContentManagerViewModel ViewModel;
public DownloadableContentManagerWindow()
{
DataContext = this;
InitializeComponent();
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
}
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
{
_virtualFileSystem = virtualFileSystem;
_downloadableContents = new AvaloniaList<DownloadableContentModel>();
_titleId = titleId;
_titleName = titleName;
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
try
{
_downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
}
catch
{
_downloadableContentContainerList = new List<DownloadableContentContainer>();
}
DataContext = this;
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId, titleName);
InitializeComponent();
RemoveButton.IsEnabled = false;
DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged;
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.DlcWindowTitle]} - {_titleName} ({_titleId:X16})";
LoadDownloadableContents();
PrintHeading();
}
private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
{
RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0);
}
private void PrintHeading()
{
Heading.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DlcWindowHeading, _downloadableContents.Count, _titleName, _titleId.ToString("X16"));
}
private void LoadDownloadableContents()
{
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
ContentDialog contentDialog = new()
{
if (File.Exists(downloadableContentContainer.ContainerPath))
{
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
_virtualFileSystem.ImportTickets(partitionFileSystem);
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
using UniqueRef<IFile> ncaFile = new();
partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
if (nca != null)
{
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
downloadableContentContainer.ContainerPath,
downloadableContentNca.FullPath,
downloadableContentNca.Enabled));
}
}
}
}
// NOTE: Save the list again to remove leftovers.
Save();
}
private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
{
try
{
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
}
catch (Exception ex)
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, containerPath));
});
}
return null;
}
private async Task AddDownloadableContent(string path)
{
if (!File.Exists(path) || _downloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
{
return;
}
using FileStream containerFile = File.OpenRead(path);
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
bool containsDownloadableContent = false;
_virtualFileSystem.ImportTickets(partitionFileSystem);
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
if (nca == null)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.PublicData)
{
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
{
break;
}
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
containsDownloadableContent = true;
}
}
if (!containsDownloadableContent)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
}
}
private void RemoveDownloadableContents(bool removeSelectedOnly = false)
{
if (removeSelectedOnly)
{
AvaloniaList<DownloadableContentModel> removedItems = new();
foreach (var item in DlcDataGrid.SelectedItems)
{
removedItems.Add(item as DownloadableContentModel);
}
DlcDataGrid.SelectedItems.Clear();
foreach (var item in removedItems)
{
_downloadableContents.RemoveAll(_downloadableContents.Where(x => x.TitleId == item.TitleId).ToList());
}
}
else
{
_downloadableContents.Clear();
}
PrintHeading();
}
public void RemoveSelected()
{
RemoveDownloadableContents(true);
}
public void RemoveAll()
{
RemoveDownloadableContents();
}
public void EnableAll()
{
foreach(var item in _downloadableContents)
{
item.Enabled = true;
}
}
public void DisableAll()
{
foreach (var item in _downloadableContents)
{
item.Enabled = false;
}
}
public async void Add()
{
OpenFileDialog dialog = new OpenFileDialog()
{
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
AllowMultiple = true
PrimaryButtonText = "",
SecondaryButtonText = "",
CloseButtonText = "",
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId, titleName),
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16"))
};
dialog.Filters.Add(new FileDialogFilter
{
Name = "NSP",
Extensions = { "nsp" }
});
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
string[] files = await dialog.ShowAsync(this);
contentDialog.Styles.Add(bottomBorder);
if (files != null)
{
foreach (string file in files)
{
await AddDownloadableContent(file);
}
}
PrintHeading();
await ContentDialogHelper.ShowAsync(contentDialog);
}
public void Save()
private void SaveAndClose(object sender, RoutedEventArgs routedEventArgs)
{
_downloadableContentContainerList.Clear();
ViewModel.Save();
((ContentDialog)Parent).Hide();
}
DownloadableContentContainer container = default;
private void Close(object sender, RoutedEventArgs e)
{
((ContentDialog)Parent).Hide();
}
foreach (DownloadableContentModel downloadableContent in _downloadableContents)
private void RemoveDLC(object sender, RoutedEventArgs e)
{
if (sender is Button button)
{
if (container.ContainerPath != downloadableContent.ContainerPath)
if (button.DataContext is DownloadableContentModel model)
{
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
ViewModel.Remove(model);
}
}
}
private void OpenLocation(object sender, RoutedEventArgs e)
{
if (sender is Button button)
{
if (button.DataContext is DownloadableContentModel model)
{
OpenHelper.LocateFile(model.ContainerPath);
}
}
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var content in e.AddedItems)
{
if (content is DownloadableContentModel model)
{
var index = ViewModel.DownloadableContents.IndexOf(model);
if (index != -1)
{
_downloadableContentContainerList.Add(container);
ViewModel.DownloadableContents[index].Enabled = true;
}
container = new DownloadableContentContainer
{
ContainerPath = downloadableContent.ContainerPath,
DownloadableContentNcaList = new List<DownloadableContentNca>()
};
}
}
container.DownloadableContentNcaList.Add(new DownloadableContentNca
foreach (var content in e.RemovedItems)
{
if (content is DownloadableContentModel model)
{
Enabled = downloadableContent.Enabled,
TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
FullPath = downloadableContent.FullPath
});
}
var index = ViewModel.DownloadableContents.IndexOf(model);
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
{
_downloadableContentContainerList.Add(container);
if (index != -1)
{
ViewModel.DownloadableContents[index].Enabled = false;
}
}
}
using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
{
downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
}
}
public void SaveAndClose()
{
Save();
Close();
}
}
}

View File

@@ -28,5 +28,10 @@ namespace Ryujinx.Cpu.Tracking
public void Reprotect(bool asDirty = false) => _impl.Reprotect(asDirty);
public bool OverlapsWith(ulong address, ulong size) => _impl.OverlapsWith(address, size);
public bool RangeEquals(CpuRegionHandle other)
{
return _impl.RealAddress == other._impl.RealAddress && _impl.RealSize == other._impl.RealSize;
}
}
}

View File

@@ -20,5 +20,15 @@ namespace Ryujinx.Graphics.GAL
{
return target == Target.Texture2DMultisample || target == Target.Texture2DMultisampleArray;
}
public static bool HasDepthOrLayers(this Target target)
{
return target == Target.Texture3D ||
target == Target.Texture1DArray ||
target == Target.Texture2DArray ||
target == Target.Texture2DMultisampleArray ||
target == Target.Cubemap ||
target == Target.CubemapArray;
}
}
}

View File

@@ -152,21 +152,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ulong ticks = _context.GetTimestamp();
float divisor = type switch
{
ReportCounterType.SamplesPassed => _channel.TextureManager.RenderTargetScale * _channel.TextureManager.RenderTargetScale,
_ => 1f
};
ICounterEvent counter = null;
void resultHandler(object evt, ulong result)
{
if (divisor != 1f)
{
result = (ulong)MathF.Ceiling(result / divisor);
}
CounterData counterData = new CounterData
{
Counter = result,

View File

@@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
public TexturePool Pool;
public int ID;
public ulong GpuAddress;
}
private GpuContext _context;
@@ -162,6 +163,11 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public bool IsView => _viewStorage != this;
/// <summary>
/// Whether or not this texture has views.
/// </summary>
public bool HasViews => _views.Count > 0;
private int _referenceCount;
private List<TexturePoolOwner> _poolOwners;
@@ -383,6 +389,17 @@ namespace Ryujinx.Graphics.Gpu.Image
DecrementReferenceCount();
}
/// <summary>
/// Replaces the texture's physical memory range. This forces tracking to regenerate.
/// </summary>
/// <param name="range">New physical memory range backing the texture</param>
public void ReplaceRange(MultiRange range)
{
Range = range;
Group.RangeChanged();
}
/// <summary>
/// Create a copy dependency to a texture that is view compatible with this one.
/// When either texture is modified, the texture data will be copied to the other to keep them in sync.
@@ -715,6 +732,8 @@ namespace Ryujinx.Graphics.Gpu.Image
height = Math.Max(height >> level, 1);
depth = Math.Max(depth >> level, 1);
int sliceDepth = single ? 1 : depth;
SpanOrArray<byte> result;
if (Info.IsLinear)
@@ -735,7 +754,7 @@ namespace Ryujinx.Graphics.Gpu.Image
width,
height,
depth,
single ? 1 : depth,
sliceDepth,
levels,
layers,
Info.FormatInfo.BlockWidth,
@@ -759,7 +778,7 @@ namespace Ryujinx.Graphics.Gpu.Image
Info.FormatInfo.BlockHeight,
width,
height,
depth,
sliceDepth,
levels,
layers,
out byte[] decoded))
@@ -771,7 +790,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (GraphicsConfig.EnableTextureRecompression)
{
decoded = BCnEncoder.EncodeBC7(decoded, width, height, depth, levels, layers);
decoded = BCnEncoder.EncodeBC7(decoded, width, height, sliceDepth, levels, layers);
}
result = decoded;
@@ -782,15 +801,15 @@ namespace Ryujinx.Graphics.Gpu.Image
{
case Format.Etc2RgbaSrgb:
case Format.Etc2RgbaUnorm:
result = ETC2Decoder.DecodeRgba(result, width, height, depth, levels, layers);
result = ETC2Decoder.DecodeRgba(result, width, height, sliceDepth, levels, layers);
break;
case Format.Etc2RgbPtaSrgb:
case Format.Etc2RgbPtaUnorm:
result = ETC2Decoder.DecodePta(result, width, height, depth, levels, layers);
result = ETC2Decoder.DecodePta(result, width, height, sliceDepth, levels, layers);
break;
case Format.Etc2RgbSrgb:
case Format.Etc2RgbUnorm:
result = ETC2Decoder.DecodeRgb(result, width, height, depth, levels, layers);
result = ETC2Decoder.DecodeRgb(result, width, height, sliceDepth, levels, layers);
break;
}
}
@@ -800,31 +819,31 @@ namespace Ryujinx.Graphics.Gpu.Image
{
case Format.Bc1RgbaSrgb:
case Format.Bc1RgbaUnorm:
result = BCnDecoder.DecodeBC1(result, width, height, depth, levels, layers);
result = BCnDecoder.DecodeBC1(result, width, height, sliceDepth, levels, layers);
break;
case Format.Bc2Srgb:
case Format.Bc2Unorm:
result = BCnDecoder.DecodeBC2(result, width, height, depth, levels, layers);
result = BCnDecoder.DecodeBC2(result, width, height, sliceDepth, levels, layers);
break;
case Format.Bc3Srgb:
case Format.Bc3Unorm:
result = BCnDecoder.DecodeBC3(result, width, height, depth, levels, layers);
result = BCnDecoder.DecodeBC3(result, width, height, sliceDepth, levels, layers);
break;
case Format.Bc4Snorm:
case Format.Bc4Unorm:
result = BCnDecoder.DecodeBC4(result, width, height, depth, levels, layers, Format == Format.Bc4Snorm);
result = BCnDecoder.DecodeBC4(result, width, height, sliceDepth, levels, layers, Format == Format.Bc4Snorm);
break;
case Format.Bc5Snorm:
case Format.Bc5Unorm:
result = BCnDecoder.DecodeBC5(result, width, height, depth, levels, layers, Format == Format.Bc5Snorm);
result = BCnDecoder.DecodeBC5(result, width, height, sliceDepth, levels, layers, Format == Format.Bc5Snorm);
break;
case Format.Bc6HSfloat:
case Format.Bc6HUfloat:
result = BCnDecoder.DecodeBC6(result, width, height, depth, levels, layers, Format == Format.Bc6HSfloat);
result = BCnDecoder.DecodeBC6(result, width, height, sliceDepth, levels, layers, Format == Format.Bc6HSfloat);
break;
case Format.Bc7Srgb:
case Format.Bc7Unorm:
result = BCnDecoder.DecodeBC7(result, width, height, depth, levels, layers);
result = BCnDecoder.DecodeBC7(result, width, height, sliceDepth, levels, layers);
break;
}
}
@@ -1484,11 +1503,12 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
/// <param name="pool">The texture pool this texture has been added to</param>
/// <param name="id">The ID of the reference to this texture in the pool</param>
public void IncrementReferenceCount(TexturePool pool, int id)
/// <param name="gpuVa">GPU VA of the pool reference</param>
public void IncrementReferenceCount(TexturePool pool, int id, ulong gpuVa)
{
lock (_poolOwners)
{
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id });
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id, GpuAddress = gpuVa });
}
_referenceCount++;
@@ -1585,6 +1605,36 @@ namespace Ryujinx.Graphics.Gpu.Image
InvalidatedSequence++;
}
/// <summary>
/// Queue updating texture mappings on the pool. Happens from another thread.
/// </summary>
public void UpdatePoolMappings()
{
lock (_poolOwners)
{
ulong address = 0;
foreach (var owner in _poolOwners)
{
if (address == 0 || address == owner.GpuAddress)
{
address = owner.GpuAddress;
owner.Pool.QueueUpdateMapping(this, owner.ID);
}
else
{
// If there is a different GPU VA mapping, prefer the first and delete the others.
owner.Pool.ForceRemove(this, owner.ID, true);
}
}
_poolOwners.Clear();
}
InvalidatedSequence++;
}
/// <summary>
/// Delete the texture if it is not used anymore.
/// The texture is considered unused when the reference count is zero,
@@ -1636,7 +1686,7 @@ namespace Ryujinx.Graphics.Gpu.Image
Group.ClearModified(unmapRange);
}
RemoveFromPools(true);
UpdatePoolMappings();
}
/// <summary>

View File

@@ -194,6 +194,39 @@ namespace Ryujinx.Graphics.Gpu.Image
_cache.Lift(texture);
}
/// <summary>
/// Attempts to update a texture's physical memory range.
/// Returns false if there is an existing texture that matches with the updated range.
/// </summary>
/// <param name="texture">Texture to update</param>
/// <param name="range">New physical memory range</param>
/// <returns>True if the mapping was updated, false otherwise</returns>
public bool UpdateMapping(Texture texture, MultiRange range)
{
// There cannot be an existing texture compatible with this mapping in the texture cache already.
int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps);
for (int i = 0; i < overlapCount; i++)
{
var other = _textureOverlaps[i];
if (texture != other &&
(texture.IsViewCompatible(other.Info, other.Range, true, other.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible ||
other.IsViewCompatible(texture.Info, texture.Range, true, texture.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible))
{
return false;
}
}
_textures.Remove(texture);
texture.ReplaceRange(range);
_textures.Add(texture);
return true;
}
/// <summary>
/// Tries to find an existing texture, or create a new one if not found.
/// </summary>

View File

@@ -39,6 +39,11 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
class TextureGroup : IDisposable
{
/// <summary>
/// Threshold of layers to force granular handles (and thus partial loading) on array/3D textures.
/// </summary>
private const int GranularLayerThreshold = 8;
private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false);
/// <summary>
@@ -116,7 +121,29 @@ namespace Ryujinx.Graphics.Gpu.Image
_allOffsets = size.AllOffsets;
_sliceSizes = size.SliceSizes;
(_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
if (Storage.Target.HasDepthOrLayers() && Storage.Info.GetSlices() > GranularLayerThreshold)
{
_hasLayerViews = true;
_hasMipViews = true;
}
else
{
(_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
// If the texture is partially mapped, fully subdivide handles immediately.
MultiRange range = Storage.Range;
for (int i = 0; i < range.Count; i++)
{
if (range.GetSubRange(i).Address == MemoryManager.PteUnmapped)
{
_hasLayerViews = true;
_hasMipViews = true;
break;
}
}
}
RecalculateHandleRegions();
}
@@ -249,7 +276,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
bool dirty = false;
bool anyModified = false;
bool anyUnmapped = false;
bool anyNotDirty = false;
for (int i = 0; i < regionCount; i++)
{
@@ -294,20 +321,21 @@ namespace Ryujinx.Graphics.Gpu.Image
dirty |= handleDirty;
}
anyUnmapped |= handleUnmapped;
if (group.NeedsCopy)
{
// The texture we copied from is still being written to. Copy from it again the next time this texture is used.
texture.SignalGroupDirty();
}
_loadNeeded[baseHandle + i] = handleDirty && !handleUnmapped;
bool loadNeeded = handleDirty && !handleUnmapped;
anyNotDirty |= !loadNeeded;
_loadNeeded[baseHandle + i] = loadNeeded;
}
if (dirty)
{
if (anyUnmapped || (_handles.Length > 1 && (anyModified || split)))
if (anyNotDirty || (_handles.Length > 1 && (anyModified || split)))
{
// Partial texture invalidation. Only update the layers/levels with dirty flags of the storage.
@@ -331,24 +359,56 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="regionCount">The number of handles to synchronize</param>
private void SynchronizePartial(int baseHandle, int regionCount)
{
int spanEndIndex = -1;
int spanBase = 0;
ReadOnlySpan<byte> dataSpan = ReadOnlySpan<byte>.Empty;
for (int i = 0; i < regionCount; i++)
{
if (_loadNeeded[baseHandle + i])
{
var info = GetHandleInformation(baseHandle + i);
// Ensure the data for this handle is loaded in the span.
if (spanEndIndex <= i - 1)
{
spanEndIndex = i;
if (_is3D)
{
// Look ahead to see how many handles need to be loaded.
for (int j = i + 1; j < regionCount; j++)
{
if (_loadNeeded[baseHandle + j])
{
spanEndIndex = j;
}
else
{
break;
}
}
}
var endInfo = spanEndIndex == i ? info : GetHandleInformation(baseHandle + spanEndIndex);
spanBase = _allOffsets[info.Index];
int spanLast = _allOffsets[endInfo.Index + endInfo.Layers * endInfo.Levels - 1];
int endOffset = Math.Min(spanLast + _sliceSizes[endInfo.BaseLevel + endInfo.Levels - 1], (int)Storage.Size);
int size = endOffset - spanBase;
dataSpan = _physicalMemory.GetSpan(Storage.Range.GetSlice((ulong)spanBase, (ulong)size));
}
// Only one of these will be greater than 1, as partial sync is only called when there are sub-image views.
for (int layer = 0; layer < info.Layers; layer++)
{
for (int level = 0; level < info.Levels; level++)
{
int offsetIndex = GetOffsetIndex(info.BaseLayer + layer, info.BaseLevel + level);
int offset = _allOffsets[offsetIndex];
int endOffset = Math.Min(offset + _sliceSizes[info.BaseLevel + level], (int)Storage.Size);
int size = endOffset - offset;
ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Storage.Range.GetSlice((ulong)offset, (ulong)size));
ReadOnlySpan<byte> data = dataSpan.Slice(offset - spanBase);
SpanOrArray<byte> result = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel + level, true);
@@ -865,8 +925,11 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <returns>A TextureGroupHandle covering the given views</returns>
private TextureGroupHandle GenerateHandles(int viewStart, int views)
{
int viewEnd = viewStart + views - 1;
(_, int lastLevel) = GetLayerLevelForView(viewEnd);
int offset = _allOffsets[viewStart];
int endOffset = (viewStart + views == _allOffsets.Length) ? (int)Storage.Size : _allOffsets[viewStart + views];
int endOffset = _allOffsets[viewEnd] + _sliceSizes[lastLevel];
int size = endOffset - offset;
var result = new List<CpuRegionHandle>();
@@ -1057,7 +1120,8 @@ namespace Ryujinx.Graphics.Gpu.Image
/// The dirty flags from the previous handles will be kept.
/// </summary>
/// <param name="handles">The handles to replace the current handles with</param>
private void ReplaceHandles(TextureGroupHandle[] handles)
/// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param>
private void ReplaceHandles(TextureGroupHandle[] handles, bool rangeChanged)
{
if (_handles != null)
{
@@ -1065,9 +1129,50 @@ namespace Ryujinx.Graphics.Gpu.Image
foreach (TextureGroupHandle groupHandle in handles)
{
foreach (CpuRegionHandle handle in groupHandle.Handles)
if (rangeChanged)
{
handle.Reprotect();
// When the storage range changes, this becomes a little different.
// If a range does not match one in the original, treat it as modified.
// It has been newly mapped and its data must be synchronized.
if (groupHandle.Handles.Length == 0)
{
continue;
}
foreach (var oldGroup in _handles)
{
if (!groupHandle.OverlapsWith(oldGroup.Offset, oldGroup.Size))
{
continue;
}
foreach (CpuRegionHandle handle in groupHandle.Handles)
{
bool hasMatch = false;
foreach (var oldHandle in oldGroup.Handles)
{
if (oldHandle.RangeEquals(handle))
{
hasMatch = true;
break;
}
}
if (hasMatch)
{
handle.Reprotect();
}
}
}
}
else
{
foreach (CpuRegionHandle handle in groupHandle.Handles)
{
handle.Reprotect();
}
}
}
@@ -1089,7 +1194,8 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary>
/// Recalculate handle regions for this texture group, and inherit existing state into the new handles.
/// </summary>
private void RecalculateHandleRegions()
/// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param>
private void RecalculateHandleRegions(bool rangeChanged = false)
{
TextureGroupHandle[] handles;
@@ -1171,7 +1277,21 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
ReplaceHandles(handles);
ReplaceHandles(handles, rangeChanged);
}
/// <summary>
/// Regenerates handles when the storage range has been remapped.
/// This forces the regions to be fully subdivided.
/// </summary>
public void RangeChanged()
{
_hasLayerViews = true;
_hasMipViews = true;
RecalculateHandleRegions(true);
SignalAllDirty();
}
/// <summary>

View File

@@ -1,9 +1,12 @@
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Texture;
using Ryujinx.Memory.Range;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Ryujinx.Graphics.Gpu.Image
{
@@ -12,8 +15,63 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool>
{
/// <summary>
/// A request to dereference a texture from a pool.
/// </summary>
private struct DereferenceRequest
{
/// <summary>
/// Whether the dereference is due to a mapping change or not.
/// </summary>
public readonly bool IsRemapped;
/// <summary>
/// The texture being dereferenced.
/// </summary>
public readonly Texture Texture;
/// <summary>
/// The ID of the pool entry this reference belonged to.
/// </summary>
public readonly int ID;
/// <summary>
/// Create a dereference request for a texture with a specific pool ID, and remapped flag.
/// </summary>
/// <param name="isRemapped">Whether the dereference is due to a mapping change or not</param>
/// <param name="texture">The texture being dereferenced</param>
/// <param name="id">The ID of the pool entry, used to restore remapped textures</param>
private DereferenceRequest(bool isRemapped, Texture texture, int id)
{
IsRemapped = isRemapped;
Texture = texture;
ID = id;
}
/// <summary>
/// Create a dereference request for a texture removal.
/// </summary>
/// <param name="texture">The texture being removed</param>
/// <returns>A texture removal dereference request</returns>
public static DereferenceRequest Remove(Texture texture)
{
return new DereferenceRequest(false, texture, 0);
}
/// <summary>
/// Create a dereference request for a texture remapping with a specific pool ID.
/// </summary>
/// <param name="texture">The texture being remapped</param>
/// <param name="id">The ID of the pool entry, used to restore remapped textures</param>
/// <returns>A remap dereference request</returns>
public static DereferenceRequest Remap(Texture texture, int id)
{
return new DereferenceRequest(true, texture, id);
}
}
private readonly GpuChannel _channel;
private readonly ConcurrentQueue<Texture> _dereferenceQueue = new ConcurrentQueue<Texture>();
private readonly ConcurrentQueue<DereferenceRequest> _dereferenceQueue = new ConcurrentQueue<DereferenceRequest>();
private TextureDescriptor _defaultDescriptor;
/// <summary>
@@ -58,7 +116,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{
TextureInfo info = GetInfo(descriptor, out int layerSize);
ProcessDereferenceQueue();
// The dereference queue can put our texture back on the cache.
if ((texture = ProcessDereferenceQueue(id)) != null)
{
return ref descriptor;
}
texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize);
@@ -69,10 +131,10 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
texture.IncrementReferenceCount(this, id);
Items[id] = texture;
texture.IncrementReferenceCount(this, id, descriptor.UnpackAddress());
DescriptorCache[id] = descriptor;
}
else
@@ -155,11 +217,14 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="deferred">If true, queue the dereference to happen on the render thread, otherwise dereference immediately</param>
public void ForceRemove(Texture texture, int id, bool deferred)
{
Items[id] = null;
var previous = Interlocked.Exchange(ref Items[id], null);
if (deferred)
{
_dereferenceQueue.Enqueue(texture);
if (previous != null)
{
_dereferenceQueue.Enqueue(DereferenceRequest.Remove(texture));
}
}
else
{
@@ -167,16 +232,91 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Queues a request to update a texture's mapping.
/// Mapping is updated later to avoid deleting the texture if it is still sparsely mapped.
/// </summary>
/// <param name="texture">Texture with potential mapping change</param>
/// <param name="id">ID in cache of texture with potential mapping change</param>
public void QueueUpdateMapping(Texture texture, int id)
{
if (Interlocked.Exchange(ref Items[id], null) == texture)
{
_dereferenceQueue.Enqueue(DereferenceRequest.Remap(texture, id));
}
}
/// <summary>
/// Process the dereference queue, decrementing the reference count for each texture in it.
/// This is used to ensure that texture disposal happens on the render thread.
/// </summary>
private void ProcessDereferenceQueue()
/// <param name="id">The ID of the entry that triggered this method</param>
/// <returns>Texture that matches the entry ID if it has been readded to the cache.</returns>
private Texture ProcessDereferenceQueue(int id = -1)
{
while (_dereferenceQueue.TryDequeue(out Texture toRemove))
while (_dereferenceQueue.TryDequeue(out DereferenceRequest request))
{
toRemove.DecrementReferenceCount();
Texture texture = request.Texture;
// Unmapped storage textures can swap their ranges. The texture must be storage with no views or dependencies.
// TODO: Would need to update ranges on views, or guarantee that ones where the range changes can be instantly deleted.
if (request.IsRemapped && texture.Group.Storage == texture && !texture.HasViews && !texture.Group.HasCopyDependencies)
{
// Has the mapping for this texture changed?
ref readonly TextureDescriptor descriptor = ref GetDescriptorRef(request.ID);
ulong address = descriptor.UnpackAddress();
MultiRange range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size);
// If the texture is not mapped at all, delete its reference.
if (range.Count == 1 && range.GetSubRange(0).Address == MemoryManager.PteUnmapped)
{
texture.DecrementReferenceCount();
continue;
}
Items[request.ID] = texture;
// Create a new pool reference, as the last one was removed on unmap.
texture.IncrementReferenceCount(this, request.ID, address);
texture.DecrementReferenceCount();
// Refetch the range. Changes since the last check could have been lost
// as the cache entry was not restored (required to queue mapping change).
range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size);
if (!range.Equals(texture.Range))
{
// Part of the texture was mapped or unmapped. Replace the range and regenerate tracking handles.
if (!_channel.MemoryManager.Physical.TextureCache.UpdateMapping(texture, range))
{
// Texture could not be remapped due to a collision, just delete it.
if (Interlocked.Exchange(ref Items[request.ID], null) != null)
{
// If this is null, a request was already queued to decrement reference.
texture.DecrementReferenceCount(this, request.ID);
}
continue;
}
}
if (request.ID == id)
{
return texture;
}
}
else
{
texture.DecrementReferenceCount();
}
}
return null;
}
/// <summary>
@@ -213,9 +353,10 @@ namespace Ryujinx.Graphics.Gpu.Image
_channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor);
}
texture.DecrementReferenceCount(this, id);
Items[id] = null;
if (Interlocked.Exchange(ref Items[id], null) != null)
{
texture.DecrementReferenceCount(this, id);
}
}
}
}

View File

@@ -178,7 +178,7 @@ namespace Ryujinx.Graphics.OpenGL
}
_pipeline.Initialize(this);
_counters.Initialize();
_counters.Initialize(_pipeline);
// 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,

View File

@@ -773,6 +773,16 @@ namespace Ryujinx.Graphics.OpenGL
_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)
{
if (!enable)

View File

@@ -10,6 +10,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
{
private const int MaxQueryRetries = 5000;
private const long DefaultValue = -1;
private const ulong HighMask = 0xFFFFFFFF00000000;
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)
{
result = Marshal.ReadInt64(_bufferMap);
return result != DefaultValue;
return WaitingForValue(result);
}
public long AwaitResult(AutoResetEvent wakeSignal = null)
@@ -76,7 +83,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
if (wakeSignal == null)
{
while (data == DefaultValue)
while (WaitingForValue(data))
{
data = Marshal.ReadInt64(_bufferMap);
}
@@ -84,10 +91,10 @@ namespace Ryujinx.Graphics.OpenGL.Queries
else
{
int iterations = 0;
while (data == DefaultValue && iterations++ < MaxQueryRetries)
while (WaitingForValue(data) && iterations++ < MaxQueryRetries)
{
data = Marshal.ReadInt64(_bufferMap);
if (data == DefaultValue)
if (WaitingForValue(data))
{
wakeSignal.WaitOne(1);
}

View File

@@ -13,6 +13,8 @@ namespace Ryujinx.Graphics.OpenGL.Queries
public CounterType Type { get; }
public bool Disposed { get; private set; }
private readonly Pipeline _pipeline;
private Queue<CounterQueueEvent> _events = new Queue<CounterQueueEvent>();
private CounterQueueEvent _current;
@@ -28,10 +30,12 @@ namespace Ryujinx.Graphics.OpenGL.Queries
private Thread _consumerThread;
internal CounterQueue(CounterType type)
internal CounterQueue(Pipeline pipeline, CounterType type)
{
Type = type;
_pipeline = pipeline;
QueryTarget glType = GetTarget(Type);
_queryPool = new Queue<BufferedQuery>(QueryPoolInitialSize);
@@ -119,7 +123,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
_current.ReserveForHostAccess();
}
_current.Complete(draws > 0);
_current.Complete(draws > 0, _pipeline.GetCounterDivisor(Type));
_events.Enqueue(_current);
_current.OnResult += resultHandler;

View File

@@ -26,6 +26,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
private object _lock = new object();
private ulong _result = ulong.MaxValue;
private double _divisor = 1f;
public CounterQueueEvent(CounterQueue queue, QueryTarget type, ulong drawIndex)
{
@@ -45,9 +46,11 @@ namespace Ryujinx.Graphics.OpenGL.Queries
ClearCounter = true;
}
internal void Complete(bool withResult)
internal void Complete(bool withResult, double divisor)
{
_counter.End(withResult);
_divisor = divisor;
}
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;

View File

@@ -14,12 +14,12 @@ namespace Ryujinx.Graphics.OpenGL.Queries
_counterQueues = new CounterQueue[count];
}
public void Initialize()
public void Initialize(Pipeline pipeline)
{
for (int index = 0; index < _counterQueues.Length; index++)
{
CounterType type = (CounterType)index;
_counterQueues[index] = new CounterQueue(type);
_counterQueues[index] = new CounterQueue(pipeline, type);
}
}

View File

@@ -112,7 +112,7 @@ namespace Ryujinx.Graphics.Texture
int outSize = GetTextureSize(
width,
height,
depth,
sliceDepth,
levels,
layers,
blockWidth,

View File

@@ -684,6 +684,16 @@ namespace Ryujinx.Graphics.Vulkan
_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)
{
return CommandBuffer.Handle == cb.Handle;

View File

@@ -12,6 +12,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
private const int MaxQueryRetries = 5000;
private const long DefaultValue = -1;
private const long DefaultValueInt = 0xFFFFFFFF;
private const ulong HighMask = 0xFFFFFFFF00000000;
private readonly Vk _api;
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)
{
result = Marshal.ReadInt64(_bufferMap);
@@ -138,7 +145,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
if (wakeSignal == null)
{
while (data == _defaultValue)
while (WaitingForValue(data))
{
data = Marshal.ReadInt64(_bufferMap);
}
@@ -146,10 +153,10 @@ namespace Ryujinx.Graphics.Vulkan.Queries
else
{
int iterations = 0;
while (data == _defaultValue && iterations++ < MaxQueryRetries)
while (WaitingForValue(data) && iterations++ < MaxQueryRetries)
{
data = Marshal.ReadInt64(_bufferMap);
if (data == _defaultValue)
if (WaitingForValue(data))
{
wakeSignal.WaitOne(1);
}

View File

@@ -148,7 +148,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
_current.ReserveForHostAccess();
}
_current.Complete(draws > 0 && Type != CounterType.TransformFeedbackPrimitivesWritten);
_current.Complete(draws > 0 && Type != CounterType.TransformFeedbackPrimitivesWritten, _pipeline.GetCounterDivisor(Type));
_events.Enqueue(_current);
_current.OnResult += resultHandler;

View File

@@ -24,6 +24,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries
private object _lock = new object();
private ulong _result = ulong.MaxValue;
private double _divisor = 1f;
public CounterQueueEvent(CounterQueue queue, CounterType type, ulong drawIndex)
{
@@ -52,9 +53,11 @@ namespace Ryujinx.Graphics.Vulkan.Queries
ClearCounter = true;
}
internal void Complete(bool withResult)
internal void Complete(bool withResult, double divisor)
{
_counter.End(withResult);
_divisor = divisor;
}
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;

View File

@@ -29,9 +29,6 @@
<PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
<PackageReference Include="System.IO.FileSystem.Primitives" />
<PackageReference Include="System.Net.NameResolution" />
<PackageReference Include="System.Threading.ThreadPool" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,4 @@
using Force.Crc32;
using Ryujinx.Common;
using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
@@ -9,6 +8,7 @@ using Ryujinx.Input.Motion.CemuHook.Protocol;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Hashing;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
@@ -401,10 +401,10 @@ namespace Ryujinx.Input.Motion.CemuHook
writer.Seek(6, SeekOrigin.Begin);
writer.Write(header.Length);
header.Crc32 = Crc32Algorithm.Compute(stream.ToArray());
Crc32.Hash(stream.ToArray(), header.Crc32.AsSpan());
writer.Seek(8, SeekOrigin.Begin);
writer.Write(header.Crc32);
writer.Write(header.Crc32.AsSpan());
byte[] data = stream.ToArray();
@@ -440,10 +440,10 @@ namespace Ryujinx.Input.Motion.CemuHook
writer.Seek(6, SeekOrigin.Begin);
writer.Write(header.Length);
header.Crc32 = Crc32Algorithm.Compute(stream.ToArray());
Crc32.Hash(stream.ToArray(), header.Crc32.AsSpan());
writer.Seek(8, SeekOrigin.Begin);
writer.Write(header.Crc32);
writer.Write(header.Crc32.AsSpan());
byte[] data = stream.ToArray();
@@ -458,8 +458,7 @@ namespace Ryujinx.Input.Motion.CemuHook
Id = (uint)clientId,
MagicString = Magic,
Version = Version,
Length = 0,
Crc32 = 0
Length = 0
};
return header;

View File

@@ -1,14 +1,15 @@
using System.Runtime.InteropServices;
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.Input.Motion.CemuHook.Protocol
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Header
{
public uint MagicString;
public uint MagicString;
public ushort Version;
public ushort Length;
public uint Crc32;
public uint Id;
public Array4<byte> Crc32;
public uint Id;
}
}

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crc32.NET" />
<PackageReference Include="System.IO.Hashing" />
</ItemGroup>
<ItemGroup>

View File

@@ -8,6 +8,8 @@ namespace Ryujinx.Memory.Range
/// </summary>
public readonly struct MultiRange : IEquatable<MultiRange>
{
private const ulong InvalidAddress = ulong.MaxValue;
private readonly MemoryRange _singleRange;
private readonly MemoryRange[] _ranges;
@@ -107,7 +109,16 @@ namespace Ryujinx.Memory.Range
else if (offset < range.Size)
{
ulong sliceSize = Math.Min(size, range.Size - offset);
ranges.Add(new MemoryRange(range.Address + offset, sliceSize));
if (range.Address == InvalidAddress)
{
ranges.Add(new MemoryRange(range.Address, sliceSize));
}
else
{
ranges.Add(new MemoryRange(range.Address + offset, sliceSize));
}
size -= sliceSize;
}

View File

@@ -6,31 +6,31 @@ PUBLISH_DIRECTORY=$1
OUTPUT_DIRECTORY=$2
ENTITLEMENTS_FILE_PATH=$3
APP_BUNDLE_DIRECTORY=$OUTPUT_DIRECTORY/Ryujinx.app
APP_BUNDLE_DIRECTORY="$OUTPUT_DIRECTORY/Ryujinx.app"
rm -rf $APP_BUNDLE_DIRECTORY
mkdir -p $APP_BUNDLE_DIRECTORY/Contents
mkdir $APP_BUNDLE_DIRECTORY/Contents/Frameworks
mkdir $APP_BUNDLE_DIRECTORY/Contents/MacOS
mkdir $APP_BUNDLE_DIRECTORY/Contents/Resources
rm -rf "$APP_BUNDLE_DIRECTORY"
mkdir -p "$APP_BUNDLE_DIRECTORY/Contents"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
# Copy executables first
cp $PUBLISH_DIRECTORY/Ryujinx.Ava $APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx
chmod u+x $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"
# Then all libraries
cp $PUBLISH_DIRECTORY/*.dylib $APP_BUNDLE_DIRECTORY/Contents/Frameworks
cp "$PUBLISH_DIRECTORY"/*.dylib "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
# Then resources
cp Info.plist $APP_BUNDLE_DIRECTORY/Contents
cp Ryujinx.icns $APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns
cp updater.sh $APP_BUNDLE_DIRECTORY/Contents/Resources/updater.sh
cp -r $PUBLISH_DIRECTORY/THIRDPARTY.md $APP_BUNDLE_DIRECTORY/Contents/Resources
cp Info.plist "$APP_BUNDLE_DIRECTORY/Contents"
cp Ryujinx.icns "$APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns"
cp updater.sh "$APP_BUNDLE_DIRECTORY/Contents/Resources/updater.sh"
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
python3 bundle_fix_up.py $APP_BUNDLE_DIRECTORY MacOS/Ryujinx
python3 bundle_fix_up.py "$APP_BUNDLE_DIRECTORY" MacOS/Ryujinx
# Now sign it
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.
# 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"
rcodesign sign --entitlements-xml-path $ENTITLEMENTS_FILE_PATH $APP_BUNDLE_DIRECTORY
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY"
else
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

View File

@@ -7,54 +7,54 @@ if [ "$#" -ne 6 ]; then
exit 1
fi
mkdir -p $1
mkdir -p $2
mkdir -p $3
mkdir -p "$1"
mkdir -p "$2"
mkdir -p "$3"
BASE_DIR=$(readlink -f $1)
TEMP_DIRECTORY=$(readlink -f $2)
OUTPUT_DIRECTORY=$(readlink -f $3)
ENTITLEMENTS_FILE_PATH=$(readlink -f $4)
BASE_DIR=$(readlink -f "$1")
TEMP_DIRECTORY=$(readlink -f "$2")
OUTPUT_DIRECTORY=$(readlink -f "$3")
ENTITLEMENTS_FILE_PATH=$(readlink -f "$4")
VERSION=$5
SOURCE_REVISION_ID=$6
RELEASE_TAR_FILE_NAME=Ryujinx-$VERSION-macos_universal.app.tar
ARM64_APP_BUNDLE=$TEMP_DIRECTORY/output_arm64/Ryujinx.app
X64_APP_BUNDLE=$TEMP_DIRECTORY/output_x64/Ryujinx.app
UNIVERSAL_APP_BUNDLE=$OUTPUT_DIRECTORY/Ryujinx.app
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
X64_APP_BUNDLE="$TEMP_DIRECTORY/output_x64/Ryujinx.app"
UNIVERSAL_APP_BUNDLE="$OUTPUT_DIRECTORY/Ryujinx.app"
EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
rm -rf $TEMP_DIRECTORY
mkdir -p $TEMP_DIRECTORY
rm -rf "$TEMP_DIRECTORY"
mkdir -p "$TEMP_DIRECTORY"
DOTNET_COMMON_ARGS="-p:DebugType=embedded -p:Version=$VERSION -p:SourceRevisionId=$SOURCE_REVISION_ID --self-contained true"
dotnet restore
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-x64 -o $TEMP_DIRECTORY/publish_x64 $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
# 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
# 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
./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
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_arm64" "$TEMP_DIRECTORY/output_arm64" "$ENTITLEMENTS_FILE_PATH"
popd
rm -rf $UNIVERSAL_APP_BUNDLE
mkdir -p $OUTPUT_DIRECTORY
rm -rf "$UNIVERSAL_APP_BUNDLE"
mkdir -p "$OUTPUT_DIRECTORY"
# Let's copy one of the two different app bundle and remove the executable
cp -R $ARM64_APP_BUNDLE $UNIVERSAL_APP_BUNDLE
rm $UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH
cp -R "$ARM64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE"
rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH"
# 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)" ];
then
@@ -69,12 +69,12 @@ else
fi
# 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
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
rm $UNIVERSAL_APP_BUNDLE/Contents/Info.plist.bck
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"
rm "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist.bck"
# Now sign it
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.
# 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"
rcodesign sign --entitlements-xml-path $ENTITLEMENTS_FILE_PATH $UNIVERSAL_APP_BUNDLE
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE"
else
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
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
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
rm $RELEASE_TAR_FILE_NAME
popd