mirror of
https://git.naxdy.org/Mirror/Ryujinx.git
synced 2025-01-11 21:29:14 +00:00
Redid all code changes as against latest master
This commit is contained in:
parent
4505a7f162
commit
bf23d183b2
14 changed files with 869 additions and 2 deletions
|
@ -73,7 +73,10 @@
|
|||
"GameListContextMenuCreateShortcut": "Create Application Shortcut",
|
||||
"GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application",
|
||||
"GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application",
|
||||
"GameListContextMenuTrimXCI": "Check and Trim XCI File",
|
||||
"GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space",
|
||||
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
||||
"StatusBarXCIFileTrimming": "Trimming XCI File '{0}'",
|
||||
"StatusBarSystemVersion": "System Version: {0}",
|
||||
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
|
||||
"LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}",
|
||||
|
@ -592,6 +595,16 @@
|
|||
"SelectDlcDialogTitle": "Select DLC files",
|
||||
"SelectUpdateDialogTitle": "Select update files",
|
||||
"SelectModDialogTitle": "Select mod directory",
|
||||
"TrimXCIFileDialogTitle": "Check and Trim XCI File",
|
||||
"TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.",
|
||||
"TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB",
|
||||
"TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details",
|
||||
"TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details",
|
||||
"TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.",
|
||||
"TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim",
|
||||
"TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details",
|
||||
"TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details",
|
||||
"TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed",
|
||||
"UserProfileWindowTitle": "User Profiles Manager",
|
||||
"CheatWindowTitle": "Cheats Manager",
|
||||
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||
|
@ -601,6 +614,7 @@
|
|||
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
||||
"ModWindowHeading": "{0} Mod(s)",
|
||||
"UserProfilesEditProfile": "Edit Selected",
|
||||
"Continue": "Continue",
|
||||
"Cancel": "Cancel",
|
||||
"Save": "Save",
|
||||
"Discard": "Discard",
|
||||
|
|
24
src/Ryujinx.Ava/Common/XCIFileTrimmerLog.cs
Normal file
24
src/Ryujinx.Ava/Common/XCIFileTrimmerLog.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using Ryujinx.Ava.UI.ViewModels;
|
||||
|
||||
namespace Ryujinx.Ava.Common
|
||||
{
|
||||
class XCIFileTrimmerLog : Ryujinx.Common.Logging.XCIFileTrimmerLog
|
||||
{
|
||||
private readonly MainWindowViewModel _viewModel;
|
||||
|
||||
public XCIFileTrimmerLog(MainWindowViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
}
|
||||
|
||||
public override void Progress(long current, long total, string text, bool complete)
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_viewModel.StatusBarProgressMaximum = (int)(total);
|
||||
_viewModel.StatusBarProgressValue = (int)(current);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,11 @@
|
|||
Header="{locale:Locale GameListContextMenuCreateShortcut}"
|
||||
IsEnabled="{Binding CreateShortcutEnabled}"
|
||||
ToolTip.Tip="{OnPlatform Default={locale:Locale GameListContextMenuCreateShortcutToolTip}, macOS={locale:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
|
||||
<MenuItem
|
||||
Click="TrimXCI_Click"
|
||||
Header="{locale:Locale GameListContextMenuTrimXCI}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenUserSaveDirectory_Click"
|
||||
|
|
|
@ -341,5 +341,15 @@ namespace Ryujinx.Ava.UI.Controls
|
|||
await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
|
||||
}
|
||||
}
|
||||
|
||||
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,6 +78,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
private bool _isAppletMenuActive;
|
||||
private int _statusBarProgressMaximum;
|
||||
private int _statusBarProgressValue;
|
||||
private string _statusBarProgressStatusText;
|
||||
private bool _statusBarProgressStatusVisible;
|
||||
private bool _isPaused;
|
||||
private bool _showContent = true;
|
||||
private bool _isLoadingIndeterminate = true;
|
||||
|
@ -357,6 +359,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
|
||||
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||
|
||||
public bool TrimXCIEnabled => Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(SelectedApplication.Path, new Common.XCIFileTrimmerLog(this));
|
||||
|
||||
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild;
|
||||
|
||||
public string LoadHeading
|
||||
|
@ -469,6 +473,28 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
}
|
||||
}
|
||||
|
||||
public bool StatusBarProgressStatusVisible
|
||||
{
|
||||
get => _statusBarProgressStatusVisible;
|
||||
set
|
||||
{
|
||||
_statusBarProgressStatusVisible = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusBarProgressStatusText
|
||||
{
|
||||
get => _statusBarProgressStatusText;
|
||||
set
|
||||
{
|
||||
_statusBarProgressStatusText = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string FifoStatusText
|
||||
{
|
||||
get => _fifoStatusText;
|
||||
|
@ -1708,6 +1734,107 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void ProcessTrimResult(String filename, Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome operationOutcome)
|
||||
{
|
||||
string notifyUser = null;
|
||||
|
||||
switch (operationOutcome)
|
||||
{
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.NoTrimNecessary:
|
||||
notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileNoTrimNecessary];
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.ReadOnlyFileCannotFix:
|
||||
notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileReadOnlyFileCannotFix];
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FreeSpaceCheckFailed:
|
||||
notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileFreeSpaceCheckFailed];
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.InvalidXCIFile:
|
||||
notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileInvalidXCIFile];
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileIOWriteError:
|
||||
notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileFileIOWriteError];
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileSizeChanged:
|
||||
notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileFileSizeChanged];
|
||||
break;
|
||||
}
|
||||
|
||||
if (notifyUser != null)
|
||||
{
|
||||
await ContentDialogHelper.CreateWarningDialog(
|
||||
LocaleManager.Instance[LocaleKeys.TrimXCIFileFailedPrimaryText],
|
||||
notifyUser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TrimXCIFile(string filename)
|
||||
{
|
||||
if (filename == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var trimmer = new Ryujinx.Common.Utilities.XCIFileTrimmer(filename, new Common.XCIFileTrimmerLog(this));
|
||||
|
||||
if (trimmer.CanBeTrimmed)
|
||||
{
|
||||
var savings = (double)trimmer.DiskSpaceSavingsB / 1024.0 / 1024.0;
|
||||
var currentFileSize = (double)trimmer.FileSizeB / 1024.0 / 1024.0;
|
||||
var cartDataSize = (double)trimmer.DataSizeB / 1024.0 / 1024.0;
|
||||
var secondaryText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TrimXCIFileDialogSecondaryText, currentFileSize, cartDataSize, savings);
|
||||
|
||||
var result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogPrimaryText],
|
||||
secondaryText,
|
||||
LocaleManager.Instance[LocaleKeys.Continue],
|
||||
LocaleManager.Instance[LocaleKeys.Cancel],
|
||||
LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogTitle]
|
||||
);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
Thread XCIFileTrimThread = new(() =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
StatusBarProgressStatusText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarXCIFileTrimming, Path.GetFileName(filename));
|
||||
StatusBarProgressStatusVisible = true;
|
||||
StatusBarProgressMaximum = 1;
|
||||
StatusBarProgressValue = 0;
|
||||
StatusBarVisible = true;
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var operationOutcome = trimmer.Trim();
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ProcessTrimResult(filename, operationOutcome);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
StatusBarProgressStatusVisible = false;
|
||||
StatusBarProgressStatusText = string.Empty;
|
||||
StatusBarVisible = false;
|
||||
});
|
||||
}
|
||||
})
|
||||
{
|
||||
Name = "GUI.XCFileTrimmerThread",
|
||||
IsBackground = true,
|
||||
};
|
||||
XCIFileTrimThread.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
IsVisible="{Binding EnableNonGameRunningControls}">
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
|
@ -60,9 +61,16 @@
|
|||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||
Text="{locale:Locale StatusBarGamesLoaded}" />
|
||||
<TextBlock
|
||||
Name="StatusBarProgressStatus"
|
||||
Grid.Column="2"
|
||||
Margin="10,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding StatusBarProgressStatusVisible}"
|
||||
Text="{Binding StatusBarProgressStatusText}" />
|
||||
<ProgressBar
|
||||
Name="LoadProgressBar"
|
||||
Grid.Column="2"
|
||||
Grid.Column="3"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SystemAccentColorLight2}"
|
||||
|
|
|
@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
|
|||
TamperMachine,
|
||||
Ui,
|
||||
Vic,
|
||||
XCIFileTrimmer
|
||||
}
|
||||
}
|
||||
|
|
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
|
||||
namespace Ryujinx.Common.Logging
|
||||
{
|
||||
public class XCIFileTrimmerLog : XCIFileTrimmer.ILog
|
||||
{
|
||||
public virtual void Progress(long current, long total, string text, bool complete)
|
||||
{
|
||||
}
|
||||
|
||||
public void Write(XCIFileTrimmer.LogType logType, string text)
|
||||
{
|
||||
switch (logType)
|
||||
{
|
||||
case XCIFileTrimmer.LogType.Info:
|
||||
Logger.Notice.Print(LogClass.XCIFileTrimmer, text);
|
||||
break;
|
||||
case XCIFileTrimmer.LogType.Warn:
|
||||
Logger.Warning?.Print(LogClass.XCIFileTrimmer, text);
|
||||
break;
|
||||
case XCIFileTrimmer.LogType.Error:
|
||||
Logger.Error?.Print(LogClass.XCIFileTrimmer, text);
|
||||
break;
|
||||
case XCIFileTrimmer.LogType.Progress:
|
||||
Logger.Info?.Print(LogClass.XCIFileTrimmer, text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
481
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
481
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
|
@ -0,0 +1,481 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Common.Utilities
|
||||
{
|
||||
internal static class Performance
|
||||
{
|
||||
internal static TimeSpan Measure(Action action)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.Stop();
|
||||
}
|
||||
|
||||
return sw.Elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public class XCIFileTrimmer
|
||||
{
|
||||
private const long BytesInAMegabyte = 1024 * 1024;
|
||||
private const int BufferSize = 8 * (int)BytesInAMegabyte;
|
||||
|
||||
private const long CartSizeMBinFormattedGB = 952;
|
||||
private const int CartKeyAreaSize = 0x1000;
|
||||
private const byte PaddingByte = 0xFF;
|
||||
private const int HeaderFilePos = 0x100;
|
||||
private const int CartSizeFilePos = 0x10D;
|
||||
private const int DataSizeFilePos = 0x118;
|
||||
private const string HeaderMagicValue = "HEAD";
|
||||
|
||||
private static readonly Dictionary<byte, long> _cartSizesGB = new()
|
||||
{
|
||||
{ 0xFA, 1 },
|
||||
{ 0xF8, 2 },
|
||||
{ 0xF0, 4 },
|
||||
{ 0xE0, 8 },
|
||||
{ 0xE1, 16 },
|
||||
{ 0xE2, 32 }
|
||||
};
|
||||
|
||||
private static long RecordsToByte(long records)
|
||||
{
|
||||
return 512 + (records * 512);
|
||||
}
|
||||
|
||||
public static bool CanTrim(string filename, ILog log = null)
|
||||
{
|
||||
if (System.IO.Path.GetExtension(filename).ToUpperInvariant() == ".XCI")
|
||||
{
|
||||
var trimmer = new XCIFileTrimmer(filename, log);
|
||||
return trimmer.CanBeTrimmed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private ILog _log;
|
||||
private string _filename;
|
||||
private FileStream _fileStream;
|
||||
private BinaryReader _binaryReader;
|
||||
private long _offsetB, _dataSizeB, _cartSizeB, _fileSizeB;
|
||||
private bool _fileOK = true;
|
||||
private bool _freeSpaceChecked = false;
|
||||
private bool _freeSpaceValid = false;
|
||||
|
||||
public enum OperationOutcome
|
||||
{
|
||||
InvalidXCIFile,
|
||||
NoTrimNecessary,
|
||||
NoUntrimPossible,
|
||||
FreeSpaceCheckFailed,
|
||||
FileIOWriteError,
|
||||
ReadOnlyFileCannotFix,
|
||||
FileSizeChanged,
|
||||
Successful
|
||||
}
|
||||
|
||||
public enum LogType
|
||||
{
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Progress
|
||||
}
|
||||
|
||||
public interface ILog
|
||||
{
|
||||
public void Write(LogType logType, string text);
|
||||
public void Progress(long current, long total, string text, bool complete);
|
||||
}
|
||||
|
||||
public bool FileOK => _fileOK;
|
||||
public bool Trimmed => _fileOK && FileSizeB < UntrimmedFileSizeB;
|
||||
public bool ContainsKeyArea => _offsetB != 0;
|
||||
public bool CanBeTrimmed => _fileOK && FileSizeB > TrimmedFileSizeB;
|
||||
public bool CanBeUntrimmed => _fileOK && FileSizeB < UntrimmedFileSizeB;
|
||||
public bool FreeSpaceChecked => _fileOK && _freeSpaceChecked;
|
||||
public bool FreeSpaceValid => _fileOK && _freeSpaceValid;
|
||||
public long DataSizeB => _dataSizeB;
|
||||
public long CartSizeB => _cartSizeB;
|
||||
public long FileSizeB => _fileSizeB;
|
||||
public long DiskSpaceSavedB => CartSizeB - FileSizeB;
|
||||
public long DiskSpaceSavingsB => CartSizeB - DataSizeB;
|
||||
public long TrimmedFileSizeB => _offsetB + _dataSizeB;
|
||||
public long UntrimmedFileSizeB => _offsetB + _cartSizeB;
|
||||
|
||||
public ILog Log
|
||||
{
|
||||
get => _log;
|
||||
set => _log = value;
|
||||
}
|
||||
|
||||
public String Filename
|
||||
{
|
||||
get => this._filename;
|
||||
set
|
||||
{
|
||||
this._filename = value;
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public long Pos
|
||||
{
|
||||
get => this._fileStream.Position;
|
||||
set => this._fileStream.Position = value;
|
||||
}
|
||||
|
||||
public XCIFileTrimmer(string path, ILog log = null)
|
||||
{
|
||||
this.Log = log;
|
||||
this.Filename = path;
|
||||
ReadHeader();
|
||||
}
|
||||
|
||||
public void CheckFreeSpace()
|
||||
{
|
||||
if (this.FreeSpaceChecked)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (this.CanBeTrimmed)
|
||||
{
|
||||
this._freeSpaceValid = false;
|
||||
|
||||
OpenReaders();
|
||||
|
||||
try
|
||||
{
|
||||
this.Pos = this.TrimmedFileSizeB;
|
||||
var buffer = new byte[BufferSize];
|
||||
var readSizeB = this.FileSizeB - this.TrimmedFileSizeB;
|
||||
var reads = readSizeB / XCIFileTrimmer.BufferSize;
|
||||
long read = 0;
|
||||
|
||||
var time = Performance.Measure(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var bytes = _fileStream.Read(buffer, 0, XCIFileTrimmer.BufferSize);
|
||||
if (bytes == 0)
|
||||
break;
|
||||
|
||||
if (buffer.Take(bytes).AsParallel().Any(b => b != XCIFileTrimmer.PaddingByte))
|
||||
{
|
||||
Log?.Write(LogType.Warn, "Free space is NOT valid");
|
||||
return;
|
||||
}
|
||||
Log?.Progress(read, reads, "Verifying file can be trimmed", false);
|
||||
read++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log?.Progress(reads, reads, "Verifying file can be trimmed", true);
|
||||
}
|
||||
});
|
||||
|
||||
if (time.TotalSeconds > 0)
|
||||
{
|
||||
Log?.Write(LogType.Info, $"Checked at {readSizeB / (double)XCIFileTrimmer.BytesInAMegabyte / time.TotalSeconds:N} Mb/sec");
|
||||
}
|
||||
|
||||
Log?.Write(LogType.Info, "Free space is valid");
|
||||
this._freeSpaceValid = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseReaders();
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Log?.Write(LogType.Warn, "There is no free space to check.");
|
||||
this._freeSpaceValid = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._freeSpaceChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected void Reset()
|
||||
{
|
||||
this._freeSpaceChecked = false;
|
||||
this._freeSpaceValid = false;
|
||||
ReadHeader();
|
||||
}
|
||||
|
||||
public OperationOutcome Trim()
|
||||
{
|
||||
if (!this.FileOK)
|
||||
{
|
||||
return OperationOutcome.InvalidXCIFile;
|
||||
}
|
||||
|
||||
if (!this.CanBeTrimmed)
|
||||
{
|
||||
return OperationOutcome.NoTrimNecessary;
|
||||
}
|
||||
|
||||
if (!this.FreeSpaceChecked)
|
||||
{
|
||||
CheckFreeSpace();
|
||||
}
|
||||
|
||||
if (!this.FreeSpaceValid)
|
||||
{
|
||||
return OperationOutcome.FreeSpaceCheckFailed;
|
||||
}
|
||||
|
||||
Log?.Write(LogType.Info, "Trimming...");
|
||||
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(this.Filename);
|
||||
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
|
||||
File.SetAttributes(this.Filename, info.Attributes & ~FileAttributes.ReadOnly);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log?.Write(LogType.Error, e.ToString());
|
||||
return OperationOutcome.ReadOnlyFileCannotFix;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.Length != this.FileSizeB)
|
||||
{
|
||||
Log?.Write(LogType.Error, "File size has changed, cannot safely trim.");
|
||||
return OperationOutcome.FileSizeChanged;
|
||||
}
|
||||
|
||||
var outfileStream = new FileStream(_filename, FileMode.Open, FileAccess.Write, FileShare.Write);
|
||||
|
||||
try
|
||||
{
|
||||
outfileStream.SetLength(this.TrimmedFileSizeB);
|
||||
return OperationOutcome.Successful;
|
||||
}
|
||||
finally
|
||||
{
|
||||
outfileStream.Close();
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log?.Write(LogType.Error, e.ToString());
|
||||
return OperationOutcome.FileIOWriteError;
|
||||
}
|
||||
}
|
||||
|
||||
public OperationOutcome Untrim()
|
||||
{
|
||||
if (!this.FileOK)
|
||||
{
|
||||
return OperationOutcome.InvalidXCIFile;
|
||||
}
|
||||
|
||||
if (!this.CanBeUntrimmed)
|
||||
{
|
||||
return OperationOutcome.NoUntrimPossible;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Log?.Write(LogType.Info, "Untrimming...");
|
||||
|
||||
var info = new FileInfo(this.Filename);
|
||||
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
|
||||
File.SetAttributes(this.Filename, info.Attributes & ~FileAttributes.ReadOnly);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log?.Write(LogType.Error, e.ToString());
|
||||
return OperationOutcome.ReadOnlyFileCannotFix;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.Length != this.FileSizeB)
|
||||
{
|
||||
Log?.Write(LogType.Error, "File size has changed, cannot safely untrim.");
|
||||
return OperationOutcome.FileSizeChanged;
|
||||
}
|
||||
|
||||
var outfileStream = new FileStream(this._filename, FileMode.Append, FileAccess.Write, FileShare.Write);
|
||||
var buffer = new byte[BufferSize];
|
||||
Array.Fill<byte>(buffer, XCIFileTrimmer.PaddingByte);
|
||||
var bytesToWriteB = this.UntrimmedFileSizeB - this.FileSizeB;
|
||||
var bytesLeftToWriteB = bytesToWriteB;
|
||||
var writes = bytesLeftToWriteB / XCIFileTrimmer.BufferSize;
|
||||
var write = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var time = Performance.Measure(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (bytesLeftToWriteB > 0)
|
||||
{
|
||||
var bytesToWrite = Math.Min(XCIFileTrimmer.BufferSize, bytesLeftToWriteB);
|
||||
outfileStream.Write(buffer, 0, (int)bytesToWrite);
|
||||
bytesLeftToWriteB -= bytesToWrite;
|
||||
Log?.Progress(write, writes, "Writing padding data...", false);
|
||||
write++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log?.Progress(write, writes, "Writing padding data...", true);
|
||||
}
|
||||
});
|
||||
|
||||
if (time.TotalSeconds > 0)
|
||||
{
|
||||
Log?.Write(LogType.Info, $"Wrote at {bytesToWriteB / (double)XCIFileTrimmer.BytesInAMegabyte / time.TotalSeconds:N} Mb/sec");
|
||||
}
|
||||
|
||||
return OperationOutcome.Successful;
|
||||
}
|
||||
finally
|
||||
{
|
||||
outfileStream.Close();
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log?.Write(LogType.Error, e.ToString());
|
||||
return OperationOutcome.FileIOWriteError;
|
||||
}
|
||||
}
|
||||
|
||||
protected void OpenReaders()
|
||||
{
|
||||
if (_binaryReader == null)
|
||||
{
|
||||
this._fileStream = new FileStream(this._filename, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
this._binaryReader = new BinaryReader(this._fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
protected void CloseReaders()
|
||||
{
|
||||
if (this._binaryReader != null && this._binaryReader.BaseStream != null)
|
||||
this._binaryReader.Close();
|
||||
this._binaryReader = null;
|
||||
this._fileStream = null;
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
private void ReadHeader()
|
||||
{
|
||||
try
|
||||
{
|
||||
OpenReaders();
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt without key area
|
||||
var success = CheckAndReadHeader(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
// Attempt with key area
|
||||
success = CheckAndReadHeader(true);
|
||||
}
|
||||
|
||||
this._fileOK = success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseReaders();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.Write(LogType.Error, ex.Message);
|
||||
this._fileOK = false;
|
||||
this._dataSizeB = 0;
|
||||
this._cartSizeB = 0;
|
||||
this._fileSizeB = 0;
|
||||
this._offsetB = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckAndReadHeader(bool assumeKeyArea)
|
||||
{
|
||||
// Read file size
|
||||
this._fileSizeB = _fileStream.Length;
|
||||
if (_fileSizeB < 32 * 1024)
|
||||
{
|
||||
Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the data size is too small");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup offset
|
||||
this._offsetB = (long)(assumeKeyArea ? XCIFileTrimmer.CartKeyAreaSize : 0);
|
||||
|
||||
// Check header
|
||||
this.Pos = _offsetB + XCIFileTrimmer.HeaderFilePos;
|
||||
var head = System.Text.Encoding.ASCII.GetString(_binaryReader.ReadBytes(4));
|
||||
if (head != XCIFileTrimmer.HeaderMagicValue)
|
||||
{
|
||||
if (!assumeKeyArea)
|
||||
{
|
||||
Log?.Write(LogType.Warn, $"Incorrect header found, file mat contain a key area...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the header is corrupted");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read Cart Size
|
||||
this.Pos = _offsetB + XCIFileTrimmer.CartSizeFilePos;
|
||||
var cartSizeId = _binaryReader.ReadByte();
|
||||
if (!_cartSizesGB.TryGetValue(cartSizeId, out long cartSizeNGB))
|
||||
{
|
||||
Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the Cartridge Size is incorrect");
|
||||
return false;
|
||||
}
|
||||
this._cartSizeB = cartSizeNGB * XCIFileTrimmer.CartSizeMBinFormattedGB * XCIFileTrimmer.BytesInAMegabyte;
|
||||
|
||||
// Read data size
|
||||
this.Pos = _offsetB + XCIFileTrimmer.DataSizeFilePos;
|
||||
var records = (long)BitConverter.ToUInt32(_binaryReader.ReadBytes(4), 0);
|
||||
this._dataSizeB = RecordsToByte(records);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -132,6 +132,7 @@ namespace Ryujinx.Ui
|
|||
[GUI] ScrolledWindow _gameTableWindow;
|
||||
[GUI] Label _gpuName;
|
||||
[GUI] Label _progressLabel;
|
||||
[GUI] Label _progressStatusLabel;
|
||||
[GUI] Label _firmwareVersionLabel;
|
||||
[GUI] Gtk.ProgressBar _progressBar;
|
||||
[GUI] Box _viewBox;
|
||||
|
@ -719,6 +720,34 @@ namespace Ryujinx.Ui
|
|||
});
|
||||
}
|
||||
|
||||
public void StartProgress(string action)
|
||||
{
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
_progressStatusLabel.Text = action;
|
||||
_progressStatusLabel.Visible = true;
|
||||
_progressBar.Fraction = 0;
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateProgress(double percentage)
|
||||
{
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
_progressBar.Fraction = percentage;
|
||||
});
|
||||
}
|
||||
|
||||
public void EndProgress()
|
||||
{
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
_progressStatusLabel.Text = String.Empty;
|
||||
_progressStatusLabel.Visible = false;
|
||||
_progressBar.Fraction = 1.0;
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateGameTable()
|
||||
{
|
||||
if (_updatingGameTable || _gameLoaded)
|
||||
|
|
|
@ -667,6 +667,22 @@
|
|||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="_progressStatusLabel">
|
||||
<property name="visible">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-left">10</property>
|
||||
<property name="margin-right">5</property>
|
||||
<property name="margin-top">2</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="label" translatable="yes"></property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkProgressBar" id="_progressBar">
|
||||
<property name="width-request">200</property>
|
||||
|
@ -680,7 +696,7 @@
|
|||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
@ -25,6 +25,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
private MenuItem _openPtcDirMenuItem;
|
||||
private MenuItem _openShaderCacheDirMenuItem;
|
||||
private MenuItem _createShortcutMenuItem;
|
||||
private MenuItem _trimXCIMenuItem;
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
|
@ -198,6 +199,15 @@ namespace Ryujinx.Ui.Widgets
|
|||
};
|
||||
_createShortcutMenuItem.Activated += CreateShortcut_Clicked;
|
||||
|
||||
//
|
||||
// _trimXCIMenuItem
|
||||
//
|
||||
_trimXCIMenuItem = new MenuItem("Check and Trim XCI File")
|
||||
{
|
||||
TooltipText = "Check and Trim XCI File to Save Disk Space."
|
||||
};
|
||||
_trimXCIMenuItem.Activated += TrimXCI_Clicked;
|
||||
|
||||
ShowComponent();
|
||||
}
|
||||
|
||||
|
@ -213,6 +223,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
_manageSubMenu.Append(_openShaderCacheDirMenuItem);
|
||||
|
||||
Add(_createShortcutMenuItem);
|
||||
Add(_trimXCIMenuItem);
|
||||
Add(new SeparatorMenuItem());
|
||||
Add(_openSaveUserDirMenuItem);
|
||||
Add(_openSaveDeviceDirMenuItem);
|
||||
|
|
|
@ -79,6 +79,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
_extractLogoMenuItem.Sensitive = hasNca;
|
||||
|
||||
_createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
|
||||
_trimXCIMenuItem.Sensitive = Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(_titleFilePath, new XCIFileTrimmerLog(_parent));
|
||||
|
||||
PopupAtPointer(null);
|
||||
}
|
||||
|
@ -640,5 +641,88 @@ namespace Ryujinx.Ui.Widgets
|
|||
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
|
||||
ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
|
||||
}
|
||||
|
||||
private void ProcessTrimResult(String filename, Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome operationOutcome)
|
||||
{
|
||||
string notifyUser = null;
|
||||
|
||||
switch (operationOutcome)
|
||||
{
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.NoTrimNecessary:
|
||||
notifyUser = "XCI File does not need to be trimmed. Check logs for further details";
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.ReadOnlyFileCannotFix:
|
||||
notifyUser = "XCI File is Read Only and could not be made writable. Check logs for further details";
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FreeSpaceCheckFailed:
|
||||
notifyUser = "XCI File has data in the free space area, it is not safe to trim";
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.InvalidXCIFile:
|
||||
notifyUser = "XCI File contains invalid data. Check logs for further details";
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileIOWriteError:
|
||||
notifyUser = "XCI File could not be opened for writing. Check logs for further details";
|
||||
break;
|
||||
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileSizeChanged:
|
||||
notifyUser = "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.";
|
||||
break;
|
||||
}
|
||||
|
||||
if (notifyUser != null)
|
||||
{
|
||||
GtkDialog.CreateWarningDialog("Trimming of the XCI file failed", notifyUser);
|
||||
}
|
||||
}
|
||||
|
||||
private void TrimXCI_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
if (_titleFilePath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var trimmer = new Ryujinx.Common.Utilities.XCIFileTrimmer(_titleFilePath, new XCIFileTrimmerLog(_parent));
|
||||
|
||||
if (trimmer.CanBeTrimmed)
|
||||
{
|
||||
var savings = (double)trimmer.DiskSpaceSavingsB / 1024.0 / 1024.0;
|
||||
var currentFileSize = (double)trimmer.FileSizeB / 1024.0 / 1024.0;
|
||||
var cartDataSize = (double)trimmer.DataSizeB / 1024.0 / 1024.0;
|
||||
|
||||
using MessageDialog confirmationDialog = GtkDialog.CreateConfirmationDialog(
|
||||
$"This function will first check the empty space and then trim the XCI File to save disk space. Continue?",
|
||||
$"Current File Size: {currentFileSize:n} MB\n" +
|
||||
$"Game Data Size: {cartDataSize:n} MB\n" +
|
||||
$"Disk Space Savings: {savings:n} MB\n"
|
||||
);
|
||||
|
||||
if (confirmationDialog.Run() == (int)ResponseType.Yes)
|
||||
{
|
||||
Thread xciFileTrimmerThread = new(() =>
|
||||
{
|
||||
_parent.StartProgress($"Trimming file '{_titleFilePath}");
|
||||
|
||||
try
|
||||
{
|
||||
var oeprationOutcome = trimmer.Trim();
|
||||
|
||||
Gtk.Application.Invoke(delegate
|
||||
{
|
||||
ProcessTrimResult(_titleFilePath, oeprationOutcome);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_parent.EndProgress();
|
||||
}
|
||||
})
|
||||
{
|
||||
Name = "GUI.XCIFileTrimmerThread",
|
||||
IsBackground = true,
|
||||
};
|
||||
xciFileTrimmerThread.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
src/Ryujinx/Ui/XCIFileTrimmerLog.cs
Normal file
27
src/Ryujinx/Ui/XCIFileTrimmerLog.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ui
|
||||
{
|
||||
public class XCIFileTrimmerLog : Ryujinx.Common.Logging.XCIFileTrimmerLog
|
||||
{
|
||||
private readonly MainWindow _mainWindow;
|
||||
|
||||
public XCIFileTrimmerLog(MainWindow mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
public override void Progress(long current, long total, string text, bool complete)
|
||||
{
|
||||
if (!complete)
|
||||
{
|
||||
_mainWindow.UpdateProgress((double)current / (double)total);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainWindow.EndProgress();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue