Compare commits

..

21 Commits

Author SHA1 Message Date
TSRBerry
4e2bb13080 Fix games freezing after initializing LDN 1021 times (#5787)
* Close handle for stateChangeEvent on Finalize

* Properly dispose NetworkClient before setting it to null
2023-10-09 13:47:47 +00:00
Ahmad Tantowi
ac4f2c1e70 Avalonia: Show aspect ratio popup options in status bar (#5780)
* Show aspect ratio selection popup in status bar

* Add aspect ratio tooltip

* Fix typo
2023-10-08 11:04:41 +02:00
sharmander
e40470bbe1 Fix return value of Get function when a result does not yet exist for the address. (#5768) 2023-10-07 17:42:10 +02:00
riperiperi
f460ecc182 GPU: Add HLE macros for popular NVN macros (#5761)
* GPU: Add HLE macros for popular NVN macros

* Remove non-vector equality check

The case where it's not hardware accelerated will do the check integer-wise anyways.

* Whitespace 😔

* Address Feedback
2023-10-06 19:55:07 -03:00
riperiperi
086564c3c8 HLE: Fix Mii crc generation and minor issues (#5766)
* HLE: Fix Mii crc generation

Validating CRCs for data and device involves calculating the crc of all data including the crc being checked, which should then be 0.

The crc should be _generated_ on all data _before_ the crc in the struct. It shouldn't include the crcs themselves.

This fixes all generated miis (eg. default) having invalid crcs. This does not affect mii maker, as that generates its own charinfo.

Does not fix MK8D crash.

* Fix other mii issues

* Fully define all fields for Nickname and Ver3StoreData

Fixes an issue where the nickname for a mii would only have the first character on some method calls.

* Add Array96 type
2023-10-06 19:23:39 -03:00
gdkchan
b6ac45d36d Fix SPIR-V call out arguments regression (#5767)
* Fix SPIR-V call out arguments regression

* Shader cache version bump
2023-10-06 00:18:30 -03:00
dependabot[bot]
7afae8c699 nuget: bump Microsoft.CodeAnalysis.CSharp from 4.6.0 to 4.7.0 (#5608)
Bumps [Microsoft.CodeAnalysis.CSharp](https://github.com/dotnet/roslyn) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/dotnet/roslyn/releases)
- [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md)
- [Commits](https://github.com/dotnet/roslyn/commits)

---
updated-dependencies:
- dependency-name: Microsoft.CodeAnalysis.CSharp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-05 13:40:03 +02:00
Marco Carvalho
7835968214 Strings should not be concatenated using '+' in a loop (#5664)
* Strings should not be concatenated using '+' in a loop

* fix IDE0090

* undo GenerateLoadOrStore

* prefer string interpolation

* Update src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs

Co-authored-by: Mary <thog@protonmail.com>

---------

Co-authored-by: Mary <thog@protonmail.com>
2023-10-05 12:41:00 +02:00
gdkchan
0aceb534cb Fix SPIR-V function calls (#5764)
* Fix SPIR-V function calls

* Shader cache version bump
2023-10-04 21:35:26 -03:00
gdkchan
a0af6e4d07 Use unique temporary variables for function call parameters on SPIR-V (#5757)
* Use unique temporary variables for function call parameters on SPIR-V

* Shader cache version bump
2023-10-04 19:46:11 -03:00
jcm
f61b7818c3 Avalonia: Add macOS check for Color Space Passthrough (#5754)
* add macOS check for color passthrough

* use existing IsMacOS property

---------

Co-authored-by: jcm <butt@butts.com>
2023-10-04 19:15:37 +02:00
gdkchan
a2a97e1b11 Implement textureSamples texture query shader instruction (#5750)
* Implement textureSamples texture query shader instruction

* Shader cache version bump
2023-10-03 22:43:11 +00:00
gdkchan
8b2625b0be Decrement nvmap reference count on surface flinger prealloc (#5753) 2023-10-02 22:13:29 +00:00
gdkchan
651e24fed9 Signal friends completion event and stub CheckBlockedUserListAvailability (#5743) 2023-09-29 13:24:44 +02:00
gdkchan
41b104d0fb Fix audio renderer compressor effect (#5742)
* Delete DecibelToLinearExtended and fix Log10 function

* Fix CopyBuffer and ClearBuffer

* Change effect states from class to struct + formatting

* Formatting

* Make UpdateLowPassFilter readonly

* More compressor fixes
2023-09-29 10:48:49 +00:00
dependabot[bot]
bc44b85b0b nuget: bump FluentAvaloniaUI from 2.0.1 to 2.0.4 (#5729)
* nuget: bump FluentAvaloniaUI from 2.0.1 to 2.0.4

Bumps [FluentAvaloniaUI](https://github.com/amwx/FluentAvalonia) from 2.0.1 to 2.0.4.
- [Commits](https://github.com/amwx/FluentAvalonia/commits)

---
updated-dependencies:
- dependency-name: FluentAvaloniaUI
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update Directory.Packages.props

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-09-28 00:15:45 +02:00
gdkchan
01c2b8097c Implement NGC service (#5681)
* Implement NGC service

* Use raw byte arrays instead of string for _wordSeparators

* Silence IDE0230 for _wordSeparators

* Try to silence warning about _rangeValuesCount not being read on SparseSet

* Make AcType enum private

* Add abstract methods and one TODO that I forgot

* PR feedback

* More PR feedback

* More PR feedback
2023-09-27 19:21:26 +02:00
dependabot[bot]
4bd2ca3f0d nuget: bump System.IdentityModel.Tokens.Jwt from 6.31.0 to 7.0.0 (#5730)
Bumps [System.IdentityModel.Tokens.Jwt](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 6.31.0 to 7.0.0.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/6.31.0...7.0.0)

---
updated-dependencies:
- dependency-name: System.IdentityModel.Tokens.Jwt
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-27 19:03:41 +02:00
riperiperi
e63157cc33 GPU: Don't create tracking handles for buffer textures (#5727)
* GPU: Don't create tracking handles for buffer textures

Buffer texture memory is handled by the buffer cache - the texture shouldn't create any tracking handles as they aren't used. This change simply makes them create and iterate 0 tracking handles, while keeping the rest of the texture group around.

This prevents a possible issue where many buffer textures are created as views of overlapping buffer ranges, and virtual regions have many dependant textures that don't actually contribute anything to handle state.

Should improve performance in Mortal Kombat 1, possibly certain UE4 games when FIFO raises to 100%.

* Fix interval tree bug

* Don't check view compatibility for buffer textures
2023-09-26 12:37:10 -03:00
Ac_K
7f2fb049f5 Ava: Fix regressions by rewriting CheckLaunchState (#5728) 2023-09-26 07:17:55 +02:00
gdkchan
4744bde0e5 Reduce the amount of descriptor pool allocations on Vulkan (#5673)
* Reduce the amount of descriptor pool allocations on Vulkan

* Formatting

* Slice can be simplified

* Make GetDescriptorPoolSizes static

* Adjust CanFit calculation so that TryAllocateDescriptorSets never fails

* Remove unused field
2023-09-26 02:00:02 +02:00
119 changed files with 5839 additions and 377 deletions

View File

@@ -3,24 +3,24 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.0.3" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.3" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.3" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.3" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.3" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0" />
<PackageVersion Include="Avalonia" Version="11.0.4" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.4" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.4" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.4" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.4" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.2" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.2" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Concentus" Version="1.1.7" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="7.14.2" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.1" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.4" />
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
<PackageVersion Include="LibHac" Version="0.18.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
@@ -44,7 +44,7 @@
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
<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.31.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />
<PackageVersion Include="System.IO.Hashing" Version="7.0.0" />
<PackageVersion Include="System.Management" Version="7.0.2" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />

View File

@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace ARMeilleure.Diagnostics
{
@@ -33,7 +34,6 @@ namespace ARMeilleure.Diagnostics
public static string Get(ulong address)
{
if (_symbols.TryGetValue(address, out string result))
{
return result;
@@ -48,13 +48,15 @@ namespace ARMeilleure.Diagnostics
ulong diff = address - symbol.Start;
ulong rem = diff % symbol.ElementSize;
result = symbol.Name + "_" + diff / symbol.ElementSize;
StringBuilder resultBuilder = new();
resultBuilder.Append($"{symbol.Name}_{diff / symbol.ElementSize}");
if (rem != 0)
{
result += "+" + rem;
resultBuilder.Append($"+{rem}");
}
result = resultBuilder.ToString();
_symbols.TryAdd(address, result);
return result;

View File

@@ -189,7 +189,7 @@ namespace ARMeilleure.Translation
{
if (start.CompareTo(node.End) < 0)
{
if (overlaps.Length >= overlapCount)
if (overlaps.Length <= overlapCount)
{
Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize);
}

View File

@@ -31,9 +31,18 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public bool IsEffectEnabled { get; }
public AuxiliaryBufferCommand(uint bufferOffset, byte inputBufferOffset, byte outputBufferOffset,
ref AuxiliaryBufferAddresses sendBufferInfo, bool isEnabled, uint countMax,
CpuAddress outputBuffer, CpuAddress inputBuffer, uint updateCount, uint writeOffset, int nodeId)
public AuxiliaryBufferCommand(
uint bufferOffset,
byte inputBufferOffset,
byte outputBufferOffset,
ref AuxiliaryBufferAddresses sendBufferInfo,
bool isEnabled,
uint countMax,
CpuAddress outputBuffer,
CpuAddress inputBuffer,
uint updateCount,
uint writeOffset,
int nodeId)
{
Enabled = true;
NodeId = nodeId;

View File

@@ -21,7 +21,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private BiquadFilterParameter _parameter;
public BiquadFilterCommand(int baseIndex, ref BiquadFilterParameter filter, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, bool needInitialization, int nodeId)
public BiquadFilterCommand(
int baseIndex,
ref BiquadFilterParameter filter,
Memory<BiquadFilterState> biquadFilterStateMemory,
int inputBufferOffset,
int outputBufferOffset,
bool needInitialization,
int nodeId)
{
_parameter = filter;
BiquadFilterState = biquadFilterStateMemory;

View File

@@ -77,7 +77,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void ClearBuffer(int index)
{
Unsafe.InitBlock((void*)GetBufferPointer(index), 0, SampleCount);
Unsafe.InitBlock((void*)GetBufferPointer(index), 0, SampleCount * sizeof(float));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -89,7 +89,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void CopyBuffer(int outputBufferIndex, int inputBufferIndex)
{
Unsafe.CopyBlock((void*)GetBufferPointer(outputBufferIndex), (void*)GetBufferPointer(inputBufferIndex), SampleCount);
Unsafe.CopyBlock((void*)GetBufferPointer(outputBufferIndex), (void*)GetBufferPointer(inputBufferIndex), SampleCount * sizeof(float));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -94,18 +94,18 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain);
float y = FloatingPointHelper.Log10(newMean) * 10.0f;
float z = 0.0f;
float z = 1.0f;
bool unknown10OutOfRange = false;
bool unknown10OutOfRange = y >= state.Unknown10;
if (newMean < 1.0e-10f)
{
z = 1.0f;
y = -100.0f;
unknown10OutOfRange = state.Unknown10 < -100.0f;
unknown10OutOfRange = state.Unknown10 <= -100.0f;
}
if (y >= state.Unknown10 || unknown10OutOfRange)
if (unknown10OutOfRange)
{
float tmpGain;
@@ -118,7 +118,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
tmpGain = (y - state.Unknown10) * ((y - state.Unknown10) * -state.CompressorGainReduction);
}
z = FloatingPointHelper.DecibelToLinearExtended(tmpGain);
z = FloatingPointHelper.DecibelToLinear(tmpGain);
}
float unknown4New = z;

View File

@@ -88,7 +88,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
Matrix2x2 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain,
delayFeedbackCrossGain, delayFeedbackBaseGain);
delayFeedbackCrossGain, delayFeedbackBaseGain);
for (int i = 0; i < sampleCount; i++)
{
@@ -125,9 +125,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
Matrix4x4 delayFeedback = new(delayFeedbackBaseGain, delayFeedbackCrossGain, delayFeedbackCrossGain, 0.0f,
delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain,
delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain);
delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain,
delayFeedbackCrossGain, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
0.0f, delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain);
for (int i = 0; i < sampleCount; i++)
@@ -172,11 +172,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision);
Matrix6x6 delayFeedback = new(delayFeedbackBaseGain, 0.0f, delayFeedbackCrossGain, 0.0f, delayFeedbackCrossGain, 0.0f,
0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain,
delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, feedbackGain, 0.0f, 0.0f,
delayFeedbackCrossGain, 0.0f, 0.0f, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
0.0f, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackBaseGain);
0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain,
delayFeedbackCrossGain, delayFeedbackCrossGain, delayFeedbackBaseGain, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, feedbackGain, 0.0f, 0.0f,
delayFeedbackCrossGain, 0.0f, 0.0f, 0.0f, delayFeedbackBaseGain, delayFeedbackCrossGain,
0.0f, delayFeedbackCrossGain, 0.0f, 0.0f, delayFeedbackCrossGain, delayFeedbackBaseGain);
for (int i = 0; i < sampleCount; i++)
{

View File

@@ -28,7 +28,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private LimiterParameter _parameter;
public LimiterCommandVersion2(uint bufferOffset, LimiterParameter parameter, Memory<LimiterState> state, Memory<EffectResultState> resultState, bool isEnabled, ulong workBuffer, int nodeId)
public LimiterCommandVersion2(
uint bufferOffset,
LimiterParameter parameter,
Memory<LimiterState> state,
Memory<EffectResultState> resultState,
bool isEnabled,
ulong workBuffer,
int nodeId)
{
Enabled = true;
NodeId = nodeId;

View File

@@ -79,53 +79,57 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbMono(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{
ProcessReverbGeneric(ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableMono,
_targetEarlyDelayLineIndicesTableMono,
_targetOutputFeedbackIndicesTableMono,
_outputIndicesTableMono);
ProcessReverbGeneric(
ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableMono,
_targetEarlyDelayLineIndicesTableMono,
_targetOutputFeedbackIndicesTableMono,
_outputIndicesTableMono);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbStereo(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{
ProcessReverbGeneric(ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableStereo,
_targetEarlyDelayLineIndicesTableStereo,
_targetOutputFeedbackIndicesTableStereo,
_outputIndicesTableStereo);
ProcessReverbGeneric(
ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableStereo,
_targetEarlyDelayLineIndicesTableStereo,
_targetOutputFeedbackIndicesTableStereo,
_outputIndicesTableStereo);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbQuadraphonic(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{
ProcessReverbGeneric(ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableQuadraphonic,
_targetEarlyDelayLineIndicesTableQuadraphonic,
_targetOutputFeedbackIndicesTableQuadraphonic,
_outputIndicesTableQuadraphonic);
ProcessReverbGeneric(
ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableQuadraphonic,
_targetEarlyDelayLineIndicesTableQuadraphonic,
_targetOutputFeedbackIndicesTableQuadraphonic,
_outputIndicesTableQuadraphonic);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessReverbSurround(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount)
{
ProcessReverbGeneric(ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableSurround,
_targetEarlyDelayLineIndicesTableSurround,
_targetOutputFeedbackIndicesTableSurround,
_outputIndicesTableSurround);
ProcessReverbGeneric(
ref state,
outputBuffers,
inputBuffers,
sampleCount,
_outputEarlyIndicesTableSurround,
_targetEarlyDelayLineIndicesTableSurround,
_targetOutputFeedbackIndicesTableSurround,
_outputIndicesTableSurround);
}
private unsafe void ProcessReverbGeneric(ref ReverbState state, ReadOnlySpan<IntPtr> outputBuffers, ReadOnlySpan<IntPtr> inputBuffers, uint sampleCount, ReadOnlySpan<int> outputEarlyIndicesTable, ReadOnlySpan<int> targetEarlyDelayLineIndicesTable, ReadOnlySpan<int> targetOutputFeedbackIndicesTable, ReadOnlySpan<int> outputIndicesTable)

View File

@@ -52,7 +52,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
{
// NOTE: Nintendo uses an approximation of log10, we don't.
// As such, we support the same ranges as Nintendo to avoid unexpected behaviours.
return MathF.Pow(10, MathF.Max(x, 1.0e-10f));
return MathF.Log10(MathF.Max(x, 1.0e-10f));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -62,7 +62,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
foreach (float input in inputs)
{
res += (input * input);
float normInput = input * (1f / 32768f);
res += normInput * normInput;
}
res /= inputs.Length;
@@ -81,19 +82,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
return MathF.Pow(10.0f, db / 20.0f);
}
/// <summary>
/// Map decibel to linear in [0, 2] range.
/// </summary>
/// <param name="db">The decibel value to convert</param>
/// <returns>Converted linear value in [0, 2] range</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float DecibelToLinearExtended(float db)
{
float tmp = MathF.Log2(DecibelToLinear(db));
return MathF.Truncate(tmp) + MathF.Pow(2.0f, tmp - MathF.Truncate(tmp));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float DegreesToRadians(float degrees)
{

View File

@@ -3,7 +3,7 @@ using Ryujinx.Audio.Renderer.Parameter.Effect;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
public class CompressorState
public struct CompressorState
{
public ExponentialMovingAverage InputMovingAverage;
public float Unknown4;
@@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
CompressorGainReduction = (1.0f - ratio) / Constants.ChannelCountMax;
Unknown10 = threshold - 1.5f;
Unknown14 = threshold + 1.5f;
OutputGain = FloatingPointHelper.DecibelToLinearExtended(parameter.OutputGain + makeupGain);
OutputGain = FloatingPointHelper.DecibelToLinear(parameter.OutputGain + makeupGain);
}
}
}

View File

@@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
public class DelayState
public struct DelayState
{
public DelayLine[] DelayLines { get; }
public float[] LowPassZ { get; set; }
@@ -53,7 +53,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
LowPassBaseGain = 1.0f - LowPassFeedbackGain;
}
public void UpdateLowPassFilter(ref float tempRawRef, uint channelCount)
public readonly void UpdateLowPassFilter(ref float tempRawRef, uint channelCount)
{
for (int i = 0; i < channelCount; i++)
{

View File

@@ -4,7 +4,7 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
public class LimiterState
public struct LimiterState
{
public ExponentialMovingAverage[] DetectorAverage;
public ExponentialMovingAverage[] CompressionGainAverage;

View File

@@ -4,7 +4,7 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
public class Reverb3dState
public struct Reverb3dState
{
private readonly float[] _fdnDelayMinTimes = new float[4] { 5.0f, 6.0f, 13.0f, 14.0f };
private readonly float[] _fdnDelayMaxTimes = new float[4] { 45.704f, 82.782f, 149.94f, 271.58f };

View File

@@ -5,7 +5,7 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
public class ReverbState
public struct ReverbState
{
private static readonly float[] _fdnDelayTimes = new float[20]
{
@@ -54,7 +54,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
// Room
0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f,
// Chamber
0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f, 0.68f, 0.68f,
0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f, 0.68f, 0.68f,
// Hall
0.50f, 0.70f, 0.70f, 0.68f, 0.50f, 0.68f, 0.68f, 0.70f, 0.68f, 0.00f,
// Cathedral

View File

@@ -327,7 +327,7 @@ namespace Ryujinx.Ava.UI.ViewModels
string imageUrl = _amiiboList.Find(amiibo => amiibo.Equals(selected)).Image;
string usageString = "";
StringBuilder usageStringBuilder = new();
for (int i = 0; i < _amiiboList.Count; i++)
{
@@ -341,20 +341,19 @@ namespace Ryujinx.Ava.UI.ViewModels
{
foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
{
usageString += Environment.NewLine +
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
writable = usageItem.Write;
}
}
}
if (usageString.Length == 0)
if (usageStringBuilder.Length == 0)
{
usageString = LocaleManager.Instance[LocaleKeys.Unknown] + ".";
usageStringBuilder.Append($"{LocaleManager.Instance[LocaleKeys.Unknown]}.");
}
Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : "")} : {usageString}";
Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : "")} : {usageStringBuilder}";
}
}

View File

@@ -1278,6 +1278,11 @@ namespace Ryujinx.Ava.UI.ViewModels
Glyph = Glyph.Grid;
}
public void SetAspectRatio(AspectRatio aspectRatio)
{
ConfigurationState.Instance.Graphics.AspectRatio.Value = aspectRatio;
}
public async Task InstallFirmwareFromFile()
{
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions

View File

@@ -147,6 +147,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTextureRecompression { get; set; }
public bool EnableMacroHLE { get; set; }
public bool EnableColorSpacePassthrough { get; set; }
public bool ColorSpacePassthroughAvailable => IsMacOS;
public bool EnableFileLog { get; set; }
public bool EnableStub { get; set; }
public bool EnableInfo { get; set; }

View File

@@ -6,6 +6,7 @@
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:config="clr-namespace:Ryujinx.Common.Configuration;assembly=Ryujinx.Common"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView"
x:DataType="viewModels:MainWindowViewModel">
@@ -112,15 +113,52 @@
Background="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
<SplitButton
Name="AspectRatioStatus"
Margin="5,0,5,0"
Padding="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="Transparent"
BorderThickness="0"
CornerRadius="0"
IsVisible="{Binding !ShowLoadProgress}"
PointerReleased="AspectRatioStatus_PointerReleased"
Text="{Binding AspectRatioStatusText}"
TextAlignment="Left" />
Content="{Binding AspectRatioStatusText}"
Click="AspectRatioStatus_OnClick"
ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
<SplitButton.Styles>
<Style Selector="Border#SeparatorBorder">
<Setter Property="Opacity" Value="0" />
</Style>
</SplitButton.Styles>
<SplitButton.Flyout>
<MenuFlyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
<MenuItem
Header="{locale:Locale SettingsTabGraphicsAspectRatio4x3}"
Command="{Binding SetAspectRatio}"
CommandParameter="{x:Static config:AspectRatio.Fixed4x3}"/>
<MenuItem
Header="{locale:Locale SettingsTabGraphicsAspectRatio16x9}"
Command="{Binding SetAspectRatio}"
CommandParameter="{x:Static config:AspectRatio.Fixed16x9}"/>
<MenuItem
Header="{locale:Locale SettingsTabGraphicsAspectRatio16x10}"
Command="{Binding SetAspectRatio}"
CommandParameter="{x:Static config:AspectRatio.Fixed16x10}"/>
<MenuItem
Header="{locale:Locale SettingsTabGraphicsAspectRatio21x9}"
Command="{Binding SetAspectRatio}"
CommandParameter="{x:Static config:AspectRatio.Fixed21x9}"/>
<MenuItem
Header="{locale:Locale SettingsTabGraphicsAspectRatio32x9}"
Command="{Binding SetAspectRatio}"
CommandParameter="{x:Static config:AspectRatio.Fixed32x9}"/>
<MenuItem
Header="{locale:Locale SettingsTabGraphicsAspectRatioStretch}"
Command="{Binding SetAspectRatio}"
CommandParameter="{x:Static config:AspectRatio.Stretched}"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
<Border
Width="2"
Height="12"

View File

@@ -43,10 +43,9 @@ namespace Ryujinx.Ava.UI.Views.Main
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
}
private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
private void AspectRatioStatus_OnClick(object sender, RoutedEventArgs e)
{
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
}

View File

@@ -73,6 +73,7 @@
<TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableColorSpacePassthrough}"
IsVisible="{Binding ColorSpacePassthroughAvailable}"
ToolTip.Tip="{locale:Locale SettingsEnableColorSpacePassthroughTooltip}">
<TextBlock Text="{locale:Locale SettingsEnableColorSpacePassthrough}" />
</CheckBox>
@@ -296,4 +297,4 @@
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@@ -265,32 +265,46 @@ namespace Ryujinx.Ava.UI.Windows
private void CheckLaunchState()
{
if (ShowKeyErrorOnLoad)
{
ShowKeyErrorOnLoad = false;
UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys).Wait();
}
if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
{
Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({LinuxHelper.VmMaxMapCount})");
if (LinuxHelper.PkExecPath is not null)
{
ShowVmMaxMapCountDialog().Wait();
Dispatcher.UIThread.Post(async () =>
{
if (OperatingSystem.IsLinux())
{
await ShowVmMaxMapCountDialog();
}
});
}
else
{
ShowVmMaxMapCountWarning().Wait();
Dispatcher.UIThread.Post(async () =>
{
if (OperatingSystem.IsLinux())
{
await ShowVmMaxMapCountWarning();
}
});
}
}
if (_deferLoad)
if (!ShowKeyErrorOnLoad)
{
_deferLoad = false;
if (_deferLoad)
{
_deferLoad = false;
ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait();
ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait();
}
}
else
{
ShowKeyErrorOnLoad = false;
Dispatcher.UIThread.Post(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
}
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))

View File

@@ -192,7 +192,7 @@ namespace Ryujinx.Common.Collections
{
if (start.CompareTo(overlap.End) < 0)
{
if (overlaps.Length >= overlapCount)
if (overlaps.Length <= overlapCount)
{
Array.Resize(ref overlaps, overlapCount + ArrayGrowthSize);
}

View File

@@ -756,6 +756,18 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array96<T> : IArray<T> where T : unmanaged
{
T _e0;
Array64<T> _other;
Array31<T> _other2;
public readonly int Length => 96;
public ref T this[int index] => ref AsSpan()[index];
[Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array127<T> : IArray<T> where T : unmanaged
{
T _e0;

View File

@@ -32,6 +32,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
/// </summary>
public ref TState State => ref _state.State;
/// <summary>
/// Current shadow state.
/// </summary>
public ref TState ShadowState => ref _shadowState.State;
/// <summary>
/// Creates a new instance of the device state, with shadow state.
/// </summary>

View File

@@ -1,7 +1,10 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.Device;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.GPFifo;
using Ryujinx.Graphics.Gpu.Engine.Threed;
using Ryujinx.Graphics.Gpu.Engine.Types;
using System;
using System.Collections.Generic;
@@ -15,9 +18,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
private const int ColorLayerCountOffset = 0x818;
private const int ColorStructSize = 0x40;
private const int ZetaLayerCountOffset = 0x1230;
private const int UniformBufferBindVertexOffset = 0x2410;
private const int FirstVertexOffset = 0x1434;
private const int IndirectIndexedDataEntrySize = 0x14;
private const int LogicOpOffset = 0x19c4;
private const int ShaderIdScratchOffset = 0x3470;
private const int ShaderAddressScratchOffset = 0x3488;
private const int UpdateConstantBufferAddressesBase = 0x34a8;
private const int UpdateConstantBufferSizesBase = 0x34bc;
private const int UpdateConstantBufferAddressCbu = 0x3460;
private readonly GPFifoProcessor _processor;
private readonly MacroHLEFunctionName _functionName;
@@ -49,6 +61,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
{
switch (_functionName)
{
case MacroHLEFunctionName.BindShaderProgram:
BindShaderProgram(state, arg0);
break;
case MacroHLEFunctionName.ClearColor:
ClearColor(state, arg0);
break;
@@ -58,6 +73,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
case MacroHLEFunctionName.DrawArraysInstanced:
DrawArraysInstanced(state, arg0);
break;
case MacroHLEFunctionName.DrawElements:
DrawElements(state, arg0);
break;
case MacroHLEFunctionName.DrawElementsInstanced:
DrawElementsInstanced(state, arg0);
break;
@@ -67,6 +85,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
case MacroHLEFunctionName.MultiDrawElementsIndirectCount:
MultiDrawElementsIndirectCount(state, arg0);
break;
case MacroHLEFunctionName.UpdateBlendState:
UpdateBlendState(state, arg0);
break;
case MacroHLEFunctionName.UpdateColorMasks:
UpdateColorMasks(state, arg0);
break;
case MacroHLEFunctionName.UpdateUniformBufferState:
UpdateUniformBufferState(state, arg0);
break;
case MacroHLEFunctionName.UpdateUniformBufferStateCbu:
UpdateUniformBufferStateCbu(state, arg0);
break;
case MacroHLEFunctionName.UpdateUniformBufferStateCbuV2:
UpdateUniformBufferStateCbuV2(state, arg0);
break;
default:
throw new NotImplementedException(_functionName.ToString());
}
@@ -75,6 +108,149 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
Fifo.Clear();
}
/// <summary>
/// Binds a shader program with the index in arg0.
/// </summary>
/// <param name="state">GPU state at the time of the call</param>
/// <param name="arg0">First argument of the call</param>
private void BindShaderProgram(IDeviceState state, int arg0)
{
int scratchOffset = ShaderIdScratchOffset + arg0 * 4;
int lastId = state.Read(scratchOffset);
int id = FetchParam().Word;
int offset = FetchParam().Word;
if (lastId == id)
{
FetchParam();
FetchParam();
return;
}
_processor.ThreedClass.SetShaderOffset(arg0, (uint)offset);
// Removes overflow on the method address into the increment portion.
// Present in the original macro.
int addrMask = unchecked((int)0xfffc0fff) << 2;
state.Write(scratchOffset & addrMask, id);
state.Write((ShaderAddressScratchOffset + arg0 * 4) & addrMask, offset);
int stage = FetchParam().Word;
uint cbAddress = (uint)FetchParam().Word;
_processor.ThreedClass.UpdateUniformBufferState(65536, cbAddress >> 24, cbAddress << 8);
int stageOffset = (stage & 0x7f) << 3;
state.Write((UniformBufferBindVertexOffset + stageOffset * 4) & addrMask, 17);
}
/// <summary>
/// Updates uniform buffer state for update or bind.
/// </summary>
/// <param name="state">GPU state at the time of the call</param>
/// <param name="arg0">First argument of the call</param>
private void UpdateUniformBufferState(IDeviceState state, int arg0)
{
uint address = (uint)state.Read(UpdateConstantBufferAddressesBase + arg0 * 4);
int size = state.Read(UpdateConstantBufferSizesBase + arg0 * 4);
_processor.ThreedClass.UpdateUniformBufferState(size, address >> 24, address << 8);
}
/// <summary>
/// Updates uniform buffer state for update.
/// </summary>
/// <param name="state">GPU state at the time of the call</param>
/// <param name="arg0">First argument of the call</param>
private void UpdateUniformBufferStateCbu(IDeviceState state, int arg0)
{
uint address = (uint)state.Read(UpdateConstantBufferAddressCbu);
UniformBufferState ubState = new()
{
Address = new()
{
High = address >> 24,
Low = address << 8
},
Size = 24320,
Offset = arg0 << 2
};
_processor.ThreedClass.UpdateUniformBufferState(ubState);
}
/// <summary>
/// Updates uniform buffer state for update.
/// </summary>
/// <param name="state">GPU state at the time of the call</param>
/// <param name="arg0">First argument of the call</param>
private void UpdateUniformBufferStateCbuV2(IDeviceState state, int arg0)
{
uint address = (uint)state.Read(UpdateConstantBufferAddressCbu);
UniformBufferState ubState = new()
{
Address = new()
{
High = address >> 24,
Low = address << 8
},
Size = 28672,
Offset = arg0 << 2
};
_processor.ThreedClass.UpdateUniformBufferState(ubState);
}
/// <summary>
/// Updates blend enable using the given argument.
/// </summary>
/// <param name="state">GPU state at the time of the call</param>
/// <param name="arg0">First argument of the call</param>
private void UpdateBlendState(IDeviceState state, int arg0)
{
state.Write(LogicOpOffset, 0);
Array8<Boolean32> enable = new();
for (int i = 0; i < 8; i++)
{
enable[i] = new Boolean32((uint)(arg0 >> (i + 8)) & 1);
}
_processor.ThreedClass.UpdateBlendEnable(ref enable);
}
/// <summary>
/// Updates color masks using the given argument and three pushed arguments.
/// </summary>
/// <param name="state">GPU state at the time of the call</param>
/// <param name="arg0">First argument of the call</param>
private void UpdateColorMasks(IDeviceState state, int arg0)
{
Array8<RtColorMask> masks = new();
int index = 0;
for (int i = 0; i < 4; i++)
{
masks[index++] = new RtColorMask((uint)arg0 & 0x1fff);
masks[index++] = new RtColorMask(((uint)arg0 >> 16) & 0x1fff);
if (i != 3)
{
arg0 = FetchParam().Word;
}
}
_processor.ThreedClass.UpdateColorMasks(ref masks);
}
/// <summary>
/// Clears one bound color target.
/// </summary>
@@ -129,6 +305,36 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
indexed: false);
}
/// <summary>
/// Performs a indexed draw.
/// </summary>
/// <param name="state">GPU state at the time of the call</param>
/// <param name="arg0">First argument of the call</param>
private void DrawElements(IDeviceState state, int arg0)
{
var topology = (PrimitiveTopology)arg0;
var indexAddressHigh = FetchParam();
var indexAddressLow = FetchParam();
var indexType = FetchParam();
var firstIndex = 0;
var indexCount = FetchParam();
_processor.ThreedClass.UpdateIndexBuffer(
(uint)indexAddressHigh.Word,
(uint)indexAddressLow.Word,
(IndexType)indexType.Word);
_processor.ThreedClass.Draw(
topology,
indexCount.Word,
1,
firstIndex,
state.Read(FirstVertexOffset),
0,
indexed: true);
}
/// <summary>
/// Performs a indexed draw.
/// </summary>

View File

@@ -6,11 +6,19 @@
enum MacroHLEFunctionName
{
None,
BindShaderProgram,
ClearColor,
ClearDepthStencil,
DrawArraysInstanced,
DrawElements,
DrawElementsInstanced,
DrawElementsIndirect,
MultiDrawElementsIndirectCount,
UpdateBlendState,
UpdateColorMasks,
UpdateUniformBufferState,
UpdateUniformBufferStateCbu,
UpdateUniformBufferStateCbuV2
}
}

View File

@@ -46,12 +46,19 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
private static readonly TableEntry[] _table = new TableEntry[]
{
new(MacroHLEFunctionName.BindShaderProgram, new Hash128(0x5d5efb912369f60b, 0x69131ed5019f08ef), 0x68),
new(MacroHLEFunctionName.ClearColor, new Hash128(0xA9FB28D1DC43645A, 0xB177E5D2EAE67FB0), 0x28),
new(MacroHLEFunctionName.ClearDepthStencil, new Hash128(0x1B96CB77D4879F4F, 0x8557032FE0C965FB), 0x24),
new(MacroHLEFunctionName.DrawArraysInstanced, new Hash128(0x197FB416269DBC26, 0x34288C01DDA82202), 0x48),
new(MacroHLEFunctionName.DrawElements, new Hash128(0x3D7F32AE6C2702A7, 0x9353C9F41C1A244D), 0x20),
new(MacroHLEFunctionName.DrawElementsInstanced, new Hash128(0x1A501FD3D54EC8E0, 0x6CF570CF79DA74D6), 0x5c),
new(MacroHLEFunctionName.DrawElementsIndirect, new Hash128(0x86A3E8E903AF8F45, 0xD35BBA07C23860A4), 0x7c),
new(MacroHLEFunctionName.MultiDrawElementsIndirectCount, new Hash128(0x890AF57ED3FB1C37, 0x35D0C95C61F5386F), 0x19C),
new(MacroHLEFunctionName.UpdateBlendState, new Hash128(0x40F6D4E7B08D7640, 0x82167BEEAECB959F), 0x28),
new(MacroHLEFunctionName.UpdateColorMasks, new Hash128(0x9EE32420B8441DFD, 0x6E7724759A57333E), 0x24),
new(MacroHLEFunctionName.UpdateUniformBufferState, new Hash128(0x8EE66706049CB0B0, 0x51C1CF906EC86F7C), 0x20),
new(MacroHLEFunctionName.UpdateUniformBufferStateCbu, new Hash128(0xA4592676A3E581A0, 0xA39E77FE19FE04AC), 0x18),
new(MacroHLEFunctionName.UpdateUniformBufferStateCbuV2, new Hash128(0x392FA750489983D4, 0x35BACE455155D2C3), 0x18)
};
/// <summary>
@@ -62,18 +69,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
/// <returns>True if the host supports the HLE macro, false otherwise</returns>
private static bool IsMacroHLESupported(Capabilities caps, MacroHLEFunctionName name)
{
if (name == MacroHLEFunctionName.ClearColor ||
name == MacroHLEFunctionName.ClearDepthStencil ||
name == MacroHLEFunctionName.DrawArraysInstanced ||
name == MacroHLEFunctionName.DrawElementsInstanced ||
name == MacroHLEFunctionName.DrawElementsIndirect)
{
return true;
}
else if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
if (name == MacroHLEFunctionName.MultiDrawElementsIndirectCount)
{
return caps.SupportsIndirectParameters;
}
else if (name != MacroHLEFunctionName.None)
{
return true;
}
return false;
}

View File

@@ -10,4 +10,22 @@ namespace Ryujinx.Graphics.Gpu.Engine
MethodPassthrough = 2,
MethodReplay = 3,
}
static class SetMmeShadowRamControlModeExtensions
{
public static bool IsTrack(this SetMmeShadowRamControlMode mode)
{
return mode == SetMmeShadowRamControlMode.MethodTrack || mode == SetMmeShadowRamControlMode.MethodTrackWithFilter;
}
public static bool IsPassthrough(this SetMmeShadowRamControlMode mode)
{
return mode == SetMmeShadowRamControlMode.MethodPassthrough;
}
public static bool IsReplay(this SetMmeShadowRamControlMode mode)
{
return mode == SetMmeShadowRamControlMode.MethodReplay;
}
}
}

View File

@@ -17,9 +17,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
class StateUpdater
{
public const int ShaderStateIndex = 26;
public const int RtColorMaskIndex = 14;
public const int RasterizerStateIndex = 15;
public const int ScissorStateIndex = 16;
public const int VertexBufferStateIndex = 0;
public const int BlendStateIndex = 2;
public const int IndexBufferStateIndex = 23;
public const int PrimitiveRestartStateIndex = 12;
public const int RenderTargetStateIndex = 27;

View File

@@ -1,12 +1,15 @@
using Ryujinx.Graphics.Device;
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.Device;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.GPFifo;
using Ryujinx.Graphics.Gpu.Engine.InlineToMemory;
using Ryujinx.Graphics.Gpu.Engine.Threed.Blender;
using Ryujinx.Graphics.Gpu.Engine.Types;
using Ryujinx.Graphics.Gpu.Synchronization;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
@@ -26,6 +29,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private readonly ConstantBufferUpdater _cbUpdater;
private readonly StateUpdater _stateUpdater;
private SetMmeShadowRamControlMode ShadowMode => _state.State.SetMmeShadowRamControlMode;
/// <summary>
/// Creates a new instance of the 3D engine class.
/// </summary>
@@ -228,6 +233,206 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_cbUpdater.Update(data);
}
/// <summary>
/// Test if two 32 byte structs are equal.
/// </summary>
/// <typeparam name="T">Type of the 32-byte struct</typeparam>
/// <param name="lhs">First struct</param>
/// <param name="rhs">Second struct</param>
/// <returns>True if equal, false otherwise</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool UnsafeEquals32Byte<T>(ref T lhs, ref T rhs) where T : unmanaged
{
if (Vector256.IsHardwareAccelerated)
{
return Vector256.EqualsAll(
Unsafe.As<T, Vector256<uint>>(ref lhs),
Unsafe.As<T, Vector256<uint>>(ref rhs)
);
}
else
{
ref var lhsVec = ref Unsafe.As<T, Vector128<uint>>(ref lhs);
ref var rhsVec = ref Unsafe.As<T, Vector128<uint>>(ref rhs);
return Vector128.EqualsAll(lhsVec, rhsVec) &&
Vector128.EqualsAll(Unsafe.Add(ref lhsVec, 1), Unsafe.Add(ref rhsVec, 1));
}
}
/// <summary>
/// Updates blend enable. Respects current shadow mode.
/// </summary>
/// <param name="masks">Blend enable</param>
public void UpdateBlendEnable(ref Array8<Boolean32> enable)
{
var shadow = ShadowMode;
ref var state = ref _state.State.BlendEnable;
if (shadow.IsReplay())
{
enable = _state.ShadowState.BlendEnable;
}
if (!UnsafeEquals32Byte(ref enable, ref state))
{
state = enable;
_stateUpdater.ForceDirty(StateUpdater.BlendStateIndex);
}
if (shadow.IsTrack())
{
_state.ShadowState.BlendEnable = enable;
}
}
/// <summary>
/// Updates color masks. Respects current shadow mode.
/// </summary>
/// <param name="masks">Color masks</param>
public void UpdateColorMasks(ref Array8<RtColorMask> masks)
{
var shadow = ShadowMode;
ref var state = ref _state.State.RtColorMask;
if (shadow.IsReplay())
{
masks = _state.ShadowState.RtColorMask;
}
if (!UnsafeEquals32Byte(ref masks, ref state))
{
state = masks;
_stateUpdater.ForceDirty(StateUpdater.RtColorMaskIndex);
}
if (shadow.IsTrack())
{
_state.ShadowState.RtColorMask = masks;
}
}
/// <summary>
/// Updates index buffer state for an indexed draw. Respects current shadow mode.
/// </summary>
/// <param name="addrHigh">High part of the address</param>
/// <param name="addrLow">Low part of the address</param>
/// <param name="type">Type of the binding</param>
public void UpdateIndexBuffer(uint addrHigh, uint addrLow, IndexType type)
{
var shadow = ShadowMode;
ref var state = ref _state.State.IndexBufferState;
if (shadow.IsReplay())
{
ref var shadowState = ref _state.ShadowState.IndexBufferState;
addrHigh = shadowState.Address.High;
addrLow = shadowState.Address.Low;
type = shadowState.Type;
}
if (state.Address.High != addrHigh || state.Address.Low != addrLow || state.Type != type)
{
state.Address.High = addrHigh;
state.Address.Low = addrLow;
state.Type = type;
_stateUpdater.ForceDirty(StateUpdater.IndexBufferStateIndex);
}
if (shadow.IsTrack())
{
ref var shadowState = ref _state.ShadowState.IndexBufferState;
shadowState.Address.High = addrHigh;
shadowState.Address.Low = addrLow;
shadowState.Type = type;
}
}
/// <summary>
/// Updates uniform buffer state for update or bind. Respects current shadow mode.
/// </summary>
/// <param name="size">Size of the binding</param>
/// <param name="addrHigh">High part of the addrsss</param>
/// <param name="addrLow">Low part of the address</param>
public void UpdateUniformBufferState(int size, uint addrHigh, uint addrLow)
{
var shadow = ShadowMode;
ref var state = ref _state.State.UniformBufferState;
if (shadow.IsReplay())
{
ref var shadowState = ref _state.ShadowState.UniformBufferState;
size = shadowState.Size;
addrHigh = shadowState.Address.High;
addrLow = shadowState.Address.Low;
}
state.Size = size;
state.Address.High = addrHigh;
state.Address.Low = addrLow;
if (shadow.IsTrack())
{
ref var shadowState = ref _state.ShadowState.UniformBufferState;
shadowState.Size = size;
shadowState.Address.High = addrHigh;
shadowState.Address.Low = addrLow;
}
}
/// <summary>
/// Updates a shader offset. Respects current shadow mode.
/// </summary>
/// <param name="index">Index of the shader to update</param>
/// <param name="offset">Offset to update with</param>
public void SetShaderOffset(int index, uint offset)
{
var shadow = ShadowMode;
ref var shaderState = ref _state.State.ShaderState[index];
if (shadow.IsReplay())
{
offset = _state.ShadowState.ShaderState[index].Offset;
}
if (shaderState.Offset != offset)
{
shaderState.Offset = offset;
_stateUpdater.ForceDirty(StateUpdater.ShaderStateIndex);
}
if (shadow.IsTrack())
{
_state.ShadowState.ShaderState[index].Offset = offset;
}
}
/// <summary>
/// Updates uniform buffer state for update. Respects current shadow mode.
/// </summary>
/// <param name="ubState">Uniform buffer state</param>
public void UpdateUniformBufferState(UniformBufferState ubState)
{
var shadow = ShadowMode;
ref var state = ref _state.State.UniformBufferState;
if (shadow.IsReplay())
{
ubState = _state.ShadowState.UniformBufferState;
}
state = ubState;
if (shadow.IsTrack())
{
_state.ShadowState.UniformBufferState = ubState;
}
}
/// <summary>
/// Launches the Inline-to-Memory DMA copy operation.
/// </summary>

View File

@@ -590,9 +590,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
struct RtColorMask
{
#pragma warning disable CS0649 // Field is never assigned to
public uint Packed;
#pragma warning restore CS0649
public RtColorMask(uint packed)
{
Packed = packed;
}
/// <summary>
/// Unpacks red channel enable.

View File

@@ -5,9 +5,12 @@
/// </summary>
readonly struct Boolean32
{
#pragma warning disable CS0649 // Field is never assigned to
private readonly uint _value;
#pragma warning restore CS0649
public Boolean32(uint value)
{
_value = value;
}
public static implicit operator bool(Boolean32 value)
{

View File

@@ -696,11 +696,14 @@ namespace Ryujinx.Graphics.Gpu.Image
}
// Find view compatible matches.
int overlapsCount;
int overlapsCount = 0;
lock (_textures)
if (info.Target != Target.TextureBuffer)
{
overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
lock (_textures)
{
overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
}
}
if (_overlapInfo.Length != _textureOverlaps.Length)

View File

@@ -79,6 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Image
private int[] _allOffsets;
private int[] _sliceSizes;
private readonly bool _is3D;
private readonly bool _isBuffer;
private bool _hasMipViews;
private bool _hasLayerViews;
private readonly int _layers;
@@ -118,6 +119,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_physicalMemory = physicalMemory;
_is3D = storage.Info.Target == Target.Texture3D;
_isBuffer = storage.Info.Target == Target.TextureBuffer;
_layers = storage.Info.GetSlices();
_levels = storage.Info.Levels;
@@ -794,7 +796,11 @@ namespace Ryujinx.Graphics.Gpu.Image
int targetLayerHandles = _hasLayerViews ? slices : 1;
int targetLevelHandles = _hasMipViews ? levels : 1;
if (_is3D)
if (_isBuffer)
{
return;
}
else if (_is3D)
{
// Future mip levels come after all layers of the last mip level. Each mipmap has less layers (depth) than the last.
@@ -1327,7 +1333,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{
TextureGroupHandle[] handles;
if (!(_hasMipViews || _hasLayerViews))
if (_isBuffer)
{
handles = Array.Empty<TextureGroupHandle>();
}
else if (!(_hasMipViews || _hasLayerViews))
{
// Single dirty region.
var cpuRegionHandles = new RegionHandle[TextureRange.Count];

View File

@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
private const uint CodeGenVersion = 5682;
private const uint CodeGenVersion = 5767;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";

View File

@@ -92,14 +92,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
private static string GetIndentation(int level)
{
string indentation = string.Empty;
StringBuilder indentationBuilder = new();
for (int index = 0; index < level; index++)
{
indentation += Tab;
indentationBuilder.Append(Tab);
}
return indentation;
return indentationBuilder.ToString();
}
}
}

View File

@@ -2,6 +2,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using Ryujinx.Graphics.Shader.StructuredIr;
using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Text;
using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenBallot;
using static Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions.InstGenCall;
@@ -67,11 +68,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
int arity = (int)(info.Type & InstType.ArityMask);
string args = string.Empty;
StringBuilder builder = new();
if (atomic && (operation.StorageKind == StorageKind.StorageBuffer || operation.StorageKind == StorageKind.SharedMemory))
{
args = GenerateLoadOrStore(context, operation, isStore: false);
builder.Append(GenerateLoadOrStore(context, operation, isStore: false));
AggregateType dstType = operation.Inst == Instruction.AtomicMaxS32 || operation.Inst == Instruction.AtomicMinS32
? AggregateType.S32
@@ -79,7 +80,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
for (int argIndex = operation.SourcesCount - arity + 2; argIndex < operation.SourcesCount; argIndex++)
{
args += ", " + GetSoureExpr(context, operation.GetSource(argIndex), dstType);
builder.Append($", {GetSoureExpr(context, operation.GetSource(argIndex), dstType)}");
}
}
else
@@ -88,16 +89,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
{
if (argIndex != 0)
{
args += ", ";
builder.Append(", ");
}
AggregateType dstType = GetSrcVarType(inst, argIndex);
args += GetSoureExpr(context, operation.GetSource(argIndex), dstType);
builder.Append(GetSoureExpr(context, operation.GetSource(argIndex), dstType));
}
}
return info.OpName + '(' + args + ')';
return $"{info.OpName}({builder})";
}
else if ((info.Type & InstType.Op) != 0)
{
@@ -184,8 +185,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
case Instruction.TextureSample:
return TextureSample(context, operation);
case Instruction.TextureSize:
return TextureSize(context, operation);
case Instruction.TextureQuerySamples:
return TextureQuerySamples(context, operation);
case Instruction.TextureQuerySize:
return TextureQuerySize(context, operation);
case Instruction.UnpackDouble2x32:
return UnpackDouble2x32(context, operation);

View File

@@ -118,7 +118,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
Add(Instruction.Subtract, InstType.OpBinary, "-", 2);
Add(Instruction.SwizzleAdd, InstType.CallTernary, HelperFunctionNames.SwizzleAdd);
Add(Instruction.TextureSample, InstType.Special);
Add(Instruction.TextureSize, InstType.Special);
Add(Instruction.TextureQuerySamples, InstType.Special);
Add(Instruction.TextureQuerySize, InstType.Special);
Add(Instruction.Truncate, InstType.CallUnary, "trunc");
Add(Instruction.UnpackDouble2x32, InstType.Special);
Add(Instruction.UnpackHalf2x16, InstType.Special);

View File

@@ -517,7 +517,33 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
return texCall;
}
public static string TextureSize(CodeGenContext context, AstOperation operation)
public static string TextureQuerySamples(CodeGenContext context, AstOperation operation)
{
AstTextureOperation texOp = (AstTextureOperation)operation;
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
// TODO: Bindless texture support. For now we just return 0.
if (isBindless)
{
return NumberFormatter.FormatInt(0);
}
bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
string indexExpr = null;
if (isIndexed)
{
indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32);
}
string samplerName = GetSamplerName(context.Properties, texOp, indexExpr);
return $"textureSamples({samplerName})";
}
public static string TextureQuerySize(CodeGenContext context, AstOperation operation)
{
AstTextureOperation texOp = (AstTextureOperation)operation;
@@ -753,17 +779,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
private static string GetMaskMultiDest(int mask)
{
string swizzle = ".";
StringBuilder swizzleBuilder = new();
swizzleBuilder.Append('.');
for (int i = 0; i < 4; i++)
{
if ((mask & (1 << i)) != 0)
{
swizzle += "xyzw"[i];
swizzleBuilder.Append("xyzw"[i]);
}
}
return swizzle;
return swizzleBuilder.ToString();
}
}
}

View File

@@ -44,7 +44,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
public StructuredFunction CurrentFunction { get; set; }
private readonly Dictionary<AstOperand, Instruction> _locals = new();
private readonly Dictionary<int, Instruction[]> _localForArgs = new();
private readonly Dictionary<int, Instruction> _funcArgs = new();
private readonly Dictionary<int, (StructuredFunction, Instruction)> _functions = new();
@@ -112,7 +111,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
IsMainFunction = isMainFunction;
MayHaveReturned = false;
_locals.Clear();
_localForArgs.Clear();
_funcArgs.Clear();
}
@@ -169,11 +167,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
_locals.Add(local, spvLocal);
}
public void DeclareLocalForArgs(int funcIndex, Instruction[] spvLocals)
{
_localForArgs.Add(funcIndex, spvLocals);
}
public void DeclareArgument(int argIndex, Instruction spvLocal)
{
_funcArgs.Add(argIndex, spvLocal);
@@ -278,11 +271,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return _locals[local];
}
public Instruction[] GetLocalForArgsPointers(int funcIndex)
{
return _localForArgs[funcIndex];
}
public Instruction GetArgumentPointer(AstOperand funcArg)
{
return _funcArgs[funcArg.Value];

View File

@@ -41,28 +41,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
}
}
public static void DeclareLocalForArgs(CodeGenContext context, List<StructuredFunction> functions)
{
for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++)
{
StructuredFunction function = functions[funcIndex];
SpvInstruction[] locals = new SpvInstruction[function.InArguments.Length];
for (int i = 0; i < function.InArguments.Length; i++)
{
var type = function.GetArgumentType(i);
var localPointerType = context.TypePointer(StorageClass.Function, context.GetType(type));
var spvLocal = context.Variable(localPointerType, StorageClass.Function);
context.AddLocalVariable(spvLocal);
locals[i] = spvLocal;
}
context.DeclareLocalForArgs(funcIndex, locals);
}
}
public static void DeclareAll(CodeGenContext context, StructuredProgramInfo info)
{
DeclareConstantBuffers(context, context.Properties.ConstantBuffers.Values);

View File

@@ -134,7 +134,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
Add(Instruction.Subtract, GenerateSubtract);
Add(Instruction.SwizzleAdd, GenerateSwizzleAdd);
Add(Instruction.TextureSample, GenerateTextureSample);
Add(Instruction.TextureSize, GenerateTextureSize);
Add(Instruction.TextureQuerySamples, GenerateTextureQuerySamples);
Add(Instruction.TextureQuerySize, GenerateTextureQuerySize);
Add(Instruction.Truncate, GenerateTruncate);
Add(Instruction.UnpackDouble2x32, GenerateUnpackDouble2x32);
Add(Instruction.UnpackHalf2x16, GenerateUnpackHalf2x16);
@@ -310,26 +311,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
var (function, spvFunc) = context.GetFunction(funcId.Value);
var args = new SpvInstruction[operation.SourcesCount - 1];
var spvLocals = context.GetLocalForArgsPointers(funcId.Value);
for (int i = 0; i < args.Length; i++)
{
var operand = operation.GetSource(i + 1);
if (i >= function.InArguments.Length)
{
args[i] = context.GetLocalPointer((AstOperand)operand);
}
else
{
var type = function.GetArgumentType(i);
var value = context.Get(type, operand);
var spvLocal = spvLocals[i];
context.Store(spvLocal, value);
args[i] = spvLocal;
}
AstOperand local = (AstOperand)operand;
Debug.Assert(local.Type == OperandType.LocalVariable);
args[i] = context.GetLocalPointer(local);
}
var retType = function.ReturnType;
@@ -1492,7 +1481,36 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return new OperationResult(swizzledResultType, result);
}
private static OperationResult GenerateTextureSize(CodeGenContext context, AstOperation operation)
private static OperationResult GenerateTextureQuerySamples(CodeGenContext context, AstOperation operation)
{
AstTextureOperation texOp = (AstTextureOperation)operation;
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
// TODO: Bindless texture support. For now we just return 0.
if (isBindless)
{
return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0));
}
bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
if (isIndexed)
{
context.GetS32(texOp.GetSource(0));
}
(var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
var image = context.Load(sampledImageType, sampledImageVariable);
image = context.Image(imageType, image);
SpvInstruction result = context.ImageQuerySamples(context.TypeS32(), image);
return new OperationResult(AggregateType.S32, result);
}
private static OperationResult GenerateTextureQuerySize(CodeGenContext context, AstOperation operation)
{
AstTextureOperation texOp = (AstTextureOperation)operation;

View File

@@ -161,7 +161,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
context.EnterBlock(function.MainBlock);
Declarations.DeclareLocals(context, function);
Declarations.DeclareLocalForArgs(context, info.Functions);
Generate(context, function.MainBlock);

View File

@@ -1094,7 +1094,14 @@ namespace Ryujinx.Graphics.Shader.Instructions
if (isBindless)
{
type = (componentMask & 4) != 0 ? SamplerType.Texture3D : SamplerType.Texture2D;
if (query == TexQuery.TexHeaderTextureType)
{
type = SamplerType.Texture2D | SamplerType.Multisample;
}
else
{
type = (componentMask & 4) != 0 ? SamplerType.Texture3D : SamplerType.Texture2D;
}
}
else
{
@@ -1102,31 +1109,69 @@ namespace Ryujinx.Graphics.Shader.Instructions
}
TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None;
int binding;
int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
Instruction.TextureSize,
type,
TextureFormat.Unknown,
flags,
TextureOperation.DefaultCbufSlot,
imm);
for (int compMask = componentMask, compIndex = 0; compMask != 0; compMask >>= 1, compIndex++)
switch (query)
{
if ((compMask & 1) != 0)
{
Operand d = GetDest();
case TexQuery.TexHeaderDimension:
binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
Instruction.TextureQuerySize,
type,
TextureFormat.Unknown,
flags,
TextureOperation.DefaultCbufSlot,
imm);
if (d == null)
for (int compMask = componentMask, compIndex = 0; compMask != 0; compMask >>= 1, compIndex++)
{
break;
if ((compMask & 1) != 0)
{
Operand d = GetDest();
if (d == null)
{
break;
}
context.Copy(d, context.TextureQuerySize(type, flags, binding, compIndex, sources));
}
}
break;
// TODO: Validate and use query parameter.
Operand res = context.TextureSize(type, flags, binding, compIndex, sources);
case TexQuery.TexHeaderTextureType:
binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
Instruction.TextureQuerySamples,
type,
TextureFormat.Unknown,
flags,
TextureOperation.DefaultCbufSlot,
imm);
context.Copy(d, res);
}
if ((componentMask & 4) != 0)
{
// Skip first 2 components if necessary.
if ((componentMask & 1) != 0)
{
GetDest();
}
if ((componentMask & 2) != 0)
{
GetDest();
}
Operand d = GetDest();
if (d != null)
{
context.Copy(d, context.TextureQuerySamples(type, flags, binding, sources));
}
}
break;
default:
context.TranslatorContext.GpuAccessor.Log($"Invalid or unsupported query type \"{query}\".");
break;
}
}

View File

@@ -1,10 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
{
[Flags]
[SuppressMessage("Design", "CA1069: Enums values should not be duplicated")]
enum Instruction
{
Absolute = 1,
@@ -118,7 +116,8 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
Subtract,
SwizzleAdd,
TextureSample,
TextureSize,
TextureQuerySamples,
TextureQuerySize,
Truncate,
UnpackDouble2x32,
UnpackHalf2x16,
@@ -160,7 +159,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
public static bool IsTextureQuery(this Instruction inst)
{
inst &= Instruction.Mask;
return inst == Instruction.Lod || inst == Instruction.TextureSize;
return inst == Instruction.Lod || inst == Instruction.TextureQuerySamples || inst == Instruction.TextureQuerySize;
}
}
}

View File

@@ -124,7 +124,8 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
Add(Instruction.Subtract, AggregateType.Scalar, AggregateType.Scalar, AggregateType.Scalar);
Add(Instruction.SwizzleAdd, AggregateType.FP32, AggregateType.FP32, AggregateType.FP32, AggregateType.S32);
Add(Instruction.TextureSample, AggregateType.FP32);
Add(Instruction.TextureSize, AggregateType.S32, AggregateType.S32, AggregateType.S32);
Add(Instruction.TextureQuerySamples, AggregateType.S32, AggregateType.S32);
Add(Instruction.TextureQuerySize, AggregateType.S32, AggregateType.S32, AggregateType.S32);
Add(Instruction.Truncate, AggregateType.Scalar, AggregateType.Scalar);
Add(Instruction.UnpackDouble2x32, AggregateType.U32, AggregateType.FP64);
Add(Instruction.UnpackHalf2x16, AggregateType.FP32, AggregateType.U32);

View File

@@ -2,17 +2,22 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
namespace Ryujinx.Graphics.Shader.StructuredIr
{
static class StructuredProgram
{
// TODO: Eventually it should be possible to specify the parameter types for the function instead of using S32 for everything.
private const AggregateType FuncParameterType = AggregateType.S32;
public static StructuredProgramInfo MakeStructuredProgram(
IReadOnlyList<Function> functions,
AttributeUsage attributeUsage,
ShaderDefinitions definitions,
ResourceManager resourceManager,
TargetLanguage targetLanguage,
bool debugMode)
{
StructuredProgramContext context = new(attributeUsage, definitions, resourceManager, debugMode);
@@ -23,19 +28,19 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
BasicBlock[] blocks = function.Blocks;
AggregateType returnType = function.ReturnsValue ? AggregateType.S32 : AggregateType.Void;
AggregateType returnType = function.ReturnsValue ? FuncParameterType : AggregateType.Void;
AggregateType[] inArguments = new AggregateType[function.InArgumentsCount];
AggregateType[] outArguments = new AggregateType[function.OutArgumentsCount];
for (int i = 0; i < inArguments.Length; i++)
{
inArguments[i] = AggregateType.S32;
inArguments[i] = FuncParameterType;
}
for (int i = 0; i < outArguments.Length; i++)
{
outArguments[i] = AggregateType.S32;
outArguments[i] = FuncParameterType;
}
context.EnterFunction(blocks.Length, function.Name, returnType, inArguments, outArguments);
@@ -58,7 +63,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
}
else
{
AddOperation(context, operation);
AddOperation(context, operation, targetLanguage, functions);
}
}
}
@@ -73,7 +78,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
return context.Info;
}
private static void AddOperation(StructuredProgramContext context, Operation operation)
private static void AddOperation(StructuredProgramContext context, Operation operation, TargetLanguage targetLanguage, IReadOnlyList<Function> functions)
{
Instruction inst = operation.Inst;
StorageKind storageKind = operation.StorageKind;
@@ -114,9 +119,43 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
IAstNode[] sources = new IAstNode[sourcesCount + outDestsCount];
for (int index = 0; index < operation.SourcesCount; index++)
if (inst == Instruction.Call && targetLanguage == TargetLanguage.Spirv)
{
sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index));
// SPIR-V requires that all function parameters are copied to a local variable before the call
// (or at least that's what the Khronos compiler does).
// First one is the function index.
Operand funcIndexOperand = operation.GetSource(0);
Debug.Assert(funcIndexOperand.Type == OperandType.Constant);
int funcIndex = funcIndexOperand.Value;
sources[0] = new AstOperand(OperandType.Constant, funcIndex);
int inArgsCount = functions[funcIndex].InArgumentsCount;
// Remaining ones are parameters, copy them to a temp local variable.
for (int index = 1; index < operation.SourcesCount; index++)
{
IAstNode source = context.GetOperandOrCbLoad(operation.GetSource(index));
if (index - 1 < inArgsCount)
{
AstOperand argTemp = context.NewTemp(FuncParameterType);
context.AddNode(new AstAssignment(argTemp, source));
sources[index] = argTemp;
}
else
{
sources[index] = source;
}
}
}
else
{
for (int index = 0; index < operation.SourcesCount; index++)
{
sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index));
}
}
for (int index = 0; index < outDestsCount; index++)

View File

@@ -897,7 +897,21 @@ namespace Ryujinx.Graphics.Shader.Translation
context.Add(new TextureOperation(Instruction.TextureSample, type, TextureFormat.Unknown, flags, binding, compMask, dests, sources));
}
public static Operand TextureSize(
public static Operand TextureQuerySamples(
this EmitterContext context,
SamplerType type,
TextureFlags flags,
int binding,
Operand[] sources)
{
Operand dest = Local();
context.Add(new TextureOperation(Instruction.TextureQuerySamples, type, TextureFormat.Unknown, flags, binding, 0, new[] { dest }, sources));
return dest;
}
public static Operand TextureQuerySize(
this EmitterContext context,
SamplerType type,
TextureFlags flags,
@@ -907,7 +921,7 @@ namespace Ryujinx.Graphics.Shader.Translation
{
Operand dest = Local();
context.Add(new TextureOperation(Instruction.TextureSize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
context.Add(new TextureOperation(Instruction.TextureQuerySize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
return dest;
}

View File

@@ -27,9 +27,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
continue;
}
if (texOp.Inst == Instruction.Lod ||
texOp.Inst == Instruction.TextureSample ||
texOp.Inst == Instruction.TextureSize)
if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery())
{
Operand bindlessHandle = Utils.FindLastOperation(texOp.GetSource(0), block);
@@ -40,7 +38,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
// as long bindless elimination is successful and we know where the texture descriptor is located.
bool rewriteSamplerType =
texOp.Type == SamplerType.TextureBuffer ||
texOp.Inst == Instruction.TextureSize;
texOp.Inst == Instruction.TextureQuerySamples ||
texOp.Inst == Instruction.TextureQuerySize;
if (bindlessHandle.Type == OperandType.ConstantBuffer)
{

View File

@@ -2,6 +2,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
namespace Ryujinx.Graphics.Shader.Translation.Optimizations
@@ -785,30 +786,31 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
private static string GetFunctionName(Operation baseOp, bool isMultiTarget, IReadOnlyList<uint> targetCbs)
{
string name = baseOp.Inst.ToString();
StringBuilder nameBuilder = new();
nameBuilder.Append(baseOp.Inst.ToString());
name += baseOp.StorageKind switch
nameBuilder.Append(baseOp.StorageKind switch
{
StorageKind.GlobalMemoryS8 => "S8",
StorageKind.GlobalMemoryS16 => "S16",
StorageKind.GlobalMemoryU8 => "U8",
StorageKind.GlobalMemoryU16 => "U16",
_ => string.Empty,
};
});
if (isMultiTarget)
{
name += "Multi";
nameBuilder.Append("Multi");
}
foreach (uint targetCb in targetCbs)
{
(int sbCbSlot, int sbCbOffset) = UnpackCbSlotAndOffset(targetCb);
name += $"_c{sbCbSlot}o{sbCbOffset}";
nameBuilder.Append($"_c{sbCbSlot}o{sbCbOffset}");
}
return name;
return nameBuilder.ToString();
}
private static bool TryGenerateStorageOp(

View File

@@ -232,8 +232,8 @@ namespace Ryujinx.Graphics.Shader.Translation
inst &= Instruction.Mask;
bool isImage = inst == Instruction.ImageLoad || inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
bool isWrite = inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
bool accurateType = inst != Instruction.Lod && inst != Instruction.TextureSize;
bool intCoords = isImage || flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureSize;
bool accurateType = !inst.IsTextureQuery();
bool intCoords = isImage || flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureQuerySize;
bool coherent = flags.HasFlag(TextureFlags.Coherent);
if (!isImage)

View File

@@ -23,7 +23,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
{
node = InsertCoordNormalization(context.Hfm, node, context.ResourceManager, context.GpuAccessor, context.Stage);
node = InsertCoordGatherBias(node, context.ResourceManager, context.GpuAccessor);
node = InsertConstOffsets(node, context.ResourceManager, context.GpuAccessor);
node = InsertConstOffsets(node, context.GpuAccessor, context.Stage);
if (texOp.Type == SamplerType.TextureBuffer && !context.GpuAccessor.QueryHostSupportsSnormBufferTextureFormat())
{
@@ -99,7 +99,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
if (texOp.Inst == Instruction.TextureSize &&
if (texOp.Inst == Instruction.TextureQuerySize &&
texOp.Index < 2 &&
!isBindless &&
!isIndexed &&
@@ -190,7 +190,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
}
LinkedListNode<INode> textureSizeNode = node.List.AddBefore(node, new TextureOperation(
Instruction.TextureSize,
Instruction.TextureQuerySize,
texOp.Type,
texOp.Format,
texOp.Flags,
@@ -259,7 +259,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
}
node.List.AddBefore(node, new TextureOperation(
Instruction.TextureSize,
Instruction.TextureQuerySize,
texOp.Type,
texOp.Format,
texOp.Flags,
@@ -287,7 +287,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
return node;
}
private static LinkedListNode<INode> InsertConstOffsets(LinkedListNode<INode> node, ResourceManager resourceManager, IGpuAccessor gpuAccessor)
private static LinkedListNode<INode> InsertConstOffsets(LinkedListNode<INode> node, IGpuAccessor gpuAccessor, ShaderStage stage)
{
// Non-constant texture offsets are not allowed (according to the spec),
// however some GPUs does support that.
@@ -440,7 +440,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
sources.CopyTo(newSources, 0);
Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage);
int destIndex = 0;
@@ -502,7 +502,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
}
else
{
Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage);
for (int index = 0; index < coordsCount; index++)
{
@@ -554,21 +554,31 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
TextureOperation texOp,
Operand[] lodSources,
Operand bindlessHandle,
int coordsCount)
int coordsCount,
ShaderStage stage)
{
Operand[] texSizes = new Operand[coordsCount];
Operand lod = Local();
Operand lod;
node.List.AddBefore(node, new TextureOperation(
Instruction.Lod,
texOp.Type,
texOp.Format,
texOp.Flags,
texOp.Binding,
0,
new[] { lod },
lodSources));
if (stage == ShaderStage.Fragment)
{
lod = Local();
node.List.AddBefore(node, new TextureOperation(
Instruction.Lod,
texOp.Type,
texOp.Format,
texOp.Flags,
texOp.Binding,
0,
new[] { lod },
lodSources));
}
else
{
lod = Const(0);
}
for (int index = 0; index < coordsCount; index++)
{
@@ -586,7 +596,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
}
node.List.AddBefore(node, new TextureOperation(
Instruction.TextureSize,
Instruction.TextureQuerySize,
texOp.Type,
texOp.Format,
texOp.Flags,

View File

@@ -329,6 +329,7 @@ namespace Ryujinx.Graphics.Shader.Translation
attributeUsage,
definitions,
resourceManager,
Options.TargetLanguage,
Options.Flags.HasFlag(TranslationFlags.DebugMode));
int geometryVerticesPerPrimitive = Definitions.OutputTopology switch

View File

@@ -27,6 +27,7 @@ namespace Ryujinx.Graphics.Vulkan
{
public bool InUse;
public bool InConsumption;
public int SubmissionCount;
public CommandBuffer CommandBuffer;
public FenceHolder Fence;
public SemaphoreHolder Semaphore;
@@ -193,6 +194,11 @@ namespace Ryujinx.Graphics.Vulkan
return _commandBuffers[cbIndex].Fence;
}
public int GetSubmissionCount(int cbIndex)
{
return _commandBuffers[cbIndex].SubmissionCount;
}
private int FreeConsumed(bool wait)
{
int freeEntry = 0;
@@ -282,6 +288,7 @@ namespace Ryujinx.Graphics.Vulkan
Debug.Assert(entry.CommandBuffer.Handle == cbs.CommandBuffer.Handle);
entry.InUse = false;
entry.InConsumption = true;
entry.SubmissionCount++;
_inUseCount--;
var commandBuffer = entry.CommandBuffer;

View File

@@ -6,7 +6,7 @@ namespace Ryujinx.Graphics.Vulkan
{
class DescriptorSetManager : IDisposable
{
private const uint DescriptorPoolMultiplier = 16;
public const uint MaxSets = 16;
public class DescriptorPoolHolder : IDisposable
{
@@ -14,36 +14,28 @@ namespace Ryujinx.Graphics.Vulkan
public Device Device { get; }
private readonly DescriptorPool _pool;
private readonly uint _capacity;
private int _freeDescriptors;
private int _totalSets;
private int _setsInUse;
private bool _done;
public unsafe DescriptorPoolHolder(Vk api, Device device)
public unsafe DescriptorPoolHolder(Vk api, Device device, ReadOnlySpan<DescriptorPoolSize> poolSizes, bool updateAfterBind)
{
Api = api;
Device = device;
var poolSizes = new[]
foreach (var poolSize in poolSizes)
{
new DescriptorPoolSize(DescriptorType.UniformBuffer, (1 + Constants.MaxUniformBufferBindings) * DescriptorPoolMultiplier),
new DescriptorPoolSize(DescriptorType.StorageBuffer, Constants.MaxStorageBufferBindings * DescriptorPoolMultiplier),
new DescriptorPoolSize(DescriptorType.CombinedImageSampler, Constants.MaxTextureBindings * DescriptorPoolMultiplier),
new DescriptorPoolSize(DescriptorType.StorageImage, Constants.MaxImageBindings * DescriptorPoolMultiplier),
new DescriptorPoolSize(DescriptorType.UniformTexelBuffer, Constants.MaxTextureBindings * DescriptorPoolMultiplier),
new DescriptorPoolSize(DescriptorType.StorageTexelBuffer, Constants.MaxImageBindings * DescriptorPoolMultiplier),
};
uint maxSets = (uint)poolSizes.Length * DescriptorPoolMultiplier;
_capacity = maxSets;
_freeDescriptors += (int)poolSize.DescriptorCount;
}
fixed (DescriptorPoolSize* pPoolsSize = poolSizes)
{
var descriptorPoolCreateInfo = new DescriptorPoolCreateInfo
{
SType = StructureType.DescriptorPoolCreateInfo,
MaxSets = maxSets,
Flags = updateAfterBind ? DescriptorPoolCreateFlags.UpdateAfterBindBit : DescriptorPoolCreateFlags.None,
MaxSets = MaxSets,
PoolSizeCount = (uint)poolSizes.Length,
PPoolSizes = pPoolsSize,
};
@@ -52,18 +44,22 @@ namespace Ryujinx.Graphics.Vulkan
}
}
public DescriptorSetCollection AllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts)
public unsafe DescriptorSetCollection AllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, int consumedDescriptors)
{
TryAllocateDescriptorSets(layouts, isTry: false, out var dsc);
TryAllocateDescriptorSets(layouts, consumedDescriptors, isTry: false, out var dsc);
return dsc;
}
public bool TryAllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, out DescriptorSetCollection dsc)
public bool TryAllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, int consumedDescriptors, out DescriptorSetCollection dsc)
{
return TryAllocateDescriptorSets(layouts, isTry: true, out dsc);
return TryAllocateDescriptorSets(layouts, consumedDescriptors, isTry: true, out dsc);
}
private unsafe bool TryAllocateDescriptorSets(ReadOnlySpan<DescriptorSetLayout> layouts, bool isTry, out DescriptorSetCollection dsc)
private unsafe bool TryAllocateDescriptorSets(
ReadOnlySpan<DescriptorSetLayout> layouts,
int consumedDescriptors,
bool isTry,
out DescriptorSetCollection dsc)
{
Debug.Assert(!_done);
@@ -84,7 +80,7 @@ namespace Ryujinx.Graphics.Vulkan
var result = Api.AllocateDescriptorSets(Device, &descriptorSetAllocateInfo, pDescriptorSets);
if (isTry && result == Result.ErrorOutOfPoolMemory)
{
_totalSets = (int)_capacity;
_totalSets = (int)MaxSets;
_done = true;
DestroyIfDone();
dsc = default;
@@ -95,6 +91,7 @@ namespace Ryujinx.Graphics.Vulkan
}
}
_freeDescriptors -= consumedDescriptors;
_totalSets += layouts.Length;
_setsInUse += layouts.Length;
@@ -109,9 +106,15 @@ namespace Ryujinx.Graphics.Vulkan
DestroyIfDone();
}
public bool CanFit(int count)
public bool CanFit(int setsCount, int descriptorsCount)
{
if (_totalSets + count <= _capacity)
// Try to determine if an allocation with the given parameters will succeed.
// An allocation may fail if the sets count or descriptors count exceeds the available counts
// of the pool.
// Not getting that right is not fatal, it will just create a new pool and try again,
// but it is less efficient.
if (_totalSets + setsCount <= MaxSets && _freeDescriptors >= descriptorsCount)
{
return true;
}
@@ -148,46 +151,74 @@ namespace Ryujinx.Graphics.Vulkan
}
private readonly Device _device;
private DescriptorPoolHolder _currentPool;
private readonly DescriptorPoolHolder[] _currentPools;
public DescriptorSetManager(Device device)
public DescriptorSetManager(Device device, int poolCount)
{
_device = device;
_currentPools = new DescriptorPoolHolder[poolCount];
}
public Auto<DescriptorSetCollection> AllocateDescriptorSet(Vk api, DescriptorSetLayout layout)
public Auto<DescriptorSetCollection> AllocateDescriptorSet(
Vk api,
DescriptorSetLayout layout,
ReadOnlySpan<DescriptorPoolSize> poolSizes,
int poolIndex,
int consumedDescriptors,
bool updateAfterBind)
{
Span<DescriptorSetLayout> layouts = stackalloc DescriptorSetLayout[1];
layouts[0] = layout;
return AllocateDescriptorSets(api, layouts);
return AllocateDescriptorSets(api, layouts, poolSizes, poolIndex, consumedDescriptors, updateAfterBind);
}
public Auto<DescriptorSetCollection> AllocateDescriptorSets(Vk api, ReadOnlySpan<DescriptorSetLayout> layouts)
public Auto<DescriptorSetCollection> AllocateDescriptorSets(
Vk api,
ReadOnlySpan<DescriptorSetLayout> layouts,
ReadOnlySpan<DescriptorPoolSize> poolSizes,
int poolIndex,
int consumedDescriptors,
bool updateAfterBind)
{
// If we fail the first time, just create a new pool and try again.
if (!GetPool(api, layouts.Length).TryAllocateDescriptorSets(layouts, out var dsc))
var pool = GetPool(api, poolSizes, poolIndex, layouts.Length, consumedDescriptors, updateAfterBind);
if (!pool.TryAllocateDescriptorSets(layouts, consumedDescriptors, out var dsc))
{
dsc = GetPool(api, layouts.Length).AllocateDescriptorSets(layouts);
pool = GetPool(api, poolSizes, poolIndex, layouts.Length, consumedDescriptors, updateAfterBind);
dsc = pool.AllocateDescriptorSets(layouts, consumedDescriptors);
}
return new Auto<DescriptorSetCollection>(dsc);
}
private DescriptorPoolHolder GetPool(Vk api, int requiredCount)
private DescriptorPoolHolder GetPool(
Vk api,
ReadOnlySpan<DescriptorPoolSize> poolSizes,
int poolIndex,
int setsCount,
int descriptorsCount,
bool updateAfterBind)
{
if (_currentPool == null || !_currentPool.CanFit(requiredCount))
ref DescriptorPoolHolder currentPool = ref _currentPools[poolIndex];
if (currentPool == null || !currentPool.CanFit(setsCount, descriptorsCount))
{
_currentPool = new DescriptorPoolHolder(api, _device);
currentPool = new DescriptorPoolHolder(api, _device, poolSizes, updateAfterBind);
}
return _currentPool;
return currentPool;
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_currentPool?.Dispose();
for (int index = 0; index < _currentPools.Length; index++)
{
_currentPools[index]?.Dispose();
_currentPools[index] = null;
}
}
}

View File

@@ -59,6 +59,8 @@ namespace Ryujinx.Graphics.Vulkan
private BitMapStruct<Array2<long>> _uniformMirrored;
private BitMapStruct<Array2<long>> _storageMirrored;
private bool _updateDescriptorCacheCbIndex;
[Flags]
private enum DirtyFlags
{
@@ -218,6 +220,7 @@ namespace Ryujinx.Graphics.Vulkan
public void SetProgram(ShaderCollection program)
{
_program = program;
_updateDescriptorCacheCbIndex = true;
_dirty = DirtyFlags.All;
}
@@ -490,7 +493,13 @@ namespace Ryujinx.Graphics.Vulkan
var dummyBuffer = _dummyBuffer?.GetBuffer();
var dsc = program.GetNewDescriptorSetCollection(_gd, cbs.CommandBufferIndex, setIndex, out var isNew).Get(cbs);
if (_updateDescriptorCacheCbIndex)
{
_updateDescriptorCacheCbIndex = false;
program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
}
var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
if (!program.HasMinimalLayout)
{
@@ -697,6 +706,7 @@ namespace Ryujinx.Graphics.Vulkan
public void SignalCommandBufferChange()
{
_updateDescriptorCacheCbIndex = true;
_dirty = DirtyFlags.All;
_uniformSet.Clear();

View File

@@ -1,5 +1,6 @@
using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -7,15 +8,28 @@ namespace Ryujinx.Graphics.Vulkan
{
class PipelineLayoutCacheEntry
{
// Those were adjusted based on current descriptor usage and the descriptor counts usually used on pipeline layouts.
// It might be a good idea to tweak them again if those change, or maybe find a way to calculate an optimal value dynamically.
private const uint DefaultUniformBufferPoolCapacity = 19 * DescriptorSetManager.MaxSets;
private const uint DefaultStorageBufferPoolCapacity = 16 * DescriptorSetManager.MaxSets;
private const uint DefaultTexturePoolCapacity = 128 * DescriptorSetManager.MaxSets;
private const uint DefaultImagePoolCapacity = 8 * DescriptorSetManager.MaxSets;
private const int MaxPoolSizesPerSet = 2;
private readonly VulkanRenderer _gd;
private readonly Device _device;
public DescriptorSetLayout[] DescriptorSetLayouts { get; }
public PipelineLayout PipelineLayout { get; }
private readonly int[] _consumedDescriptorsPerSet;
private readonly List<Auto<DescriptorSetCollection>>[][] _dsCache;
private List<Auto<DescriptorSetCollection>>[] _currentDsCache;
private readonly int[] _dsCacheCursor;
private int _dsLastCbIndex;
private int _dsLastSubmissionCount;
private PipelineLayoutCacheEntry(VulkanRenderer gd, Device device, int setsCount)
{
@@ -44,29 +58,55 @@ namespace Ryujinx.Graphics.Vulkan
bool usePushDescriptors) : this(gd, device, setDescriptors.Count)
{
(DescriptorSetLayouts, PipelineLayout) = PipelineLayoutFactory.Create(gd, device, setDescriptors, usePushDescriptors);
_consumedDescriptorsPerSet = new int[setDescriptors.Count];
for (int setIndex = 0; setIndex < setDescriptors.Count; setIndex++)
{
int count = 0;
foreach (var descriptor in setDescriptors[setIndex].Descriptors)
{
count += descriptor.Count;
}
_consumedDescriptorsPerSet[setIndex] = count;
}
}
public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(
VulkanRenderer gd,
int commandBufferIndex,
int setIndex,
out bool isNew)
public void UpdateCommandBufferIndex(int commandBufferIndex)
{
if (_dsLastCbIndex != commandBufferIndex)
int submissionCount = _gd.CommandBufferPool.GetSubmissionCount(commandBufferIndex);
if (_dsLastCbIndex != commandBufferIndex || _dsLastSubmissionCount != submissionCount)
{
_dsLastCbIndex = commandBufferIndex;
for (int i = 0; i < _dsCacheCursor.Length; i++)
{
_dsCacheCursor[i] = 0;
}
_dsLastSubmissionCount = submissionCount;
Array.Clear(_dsCacheCursor);
}
var list = _dsCache[commandBufferIndex][setIndex];
_currentDsCache = _dsCache[commandBufferIndex];
}
public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(int setIndex, out bool isNew)
{
var list = _currentDsCache[setIndex];
int index = _dsCacheCursor[setIndex]++;
if (index == list.Count)
{
var dsc = gd.DescriptorSetManager.AllocateDescriptorSet(gd.Api, DescriptorSetLayouts[setIndex]);
Span<DescriptorPoolSize> poolSizes = stackalloc DescriptorPoolSize[MaxPoolSizesPerSet];
poolSizes = GetDescriptorPoolSizes(poolSizes, setIndex);
int consumedDescriptors = _consumedDescriptorsPerSet[setIndex];
var dsc = _gd.DescriptorSetManager.AllocateDescriptorSet(
_gd.Api,
DescriptorSetLayouts[setIndex],
poolSizes,
setIndex,
consumedDescriptors,
false);
list.Add(dsc);
isNew = true;
return dsc;
@@ -76,6 +116,33 @@ namespace Ryujinx.Graphics.Vulkan
return list[index];
}
private static Span<DescriptorPoolSize> GetDescriptorPoolSizes(Span<DescriptorPoolSize> output, int setIndex)
{
int count = 1;
switch (setIndex)
{
case PipelineBase.UniformSetIndex:
output[0] = new(DescriptorType.UniformBuffer, DefaultUniformBufferPoolCapacity);
break;
case PipelineBase.StorageSetIndex:
output[0] = new(DescriptorType.StorageBuffer, DefaultStorageBufferPoolCapacity);
break;
case PipelineBase.TextureSetIndex:
output[0] = new(DescriptorType.CombinedImageSampler, DefaultTexturePoolCapacity);
output[1] = new(DescriptorType.UniformTexelBuffer, DefaultTexturePoolCapacity);
count = 2;
break;
case PipelineBase.ImageSetIndex:
output[0] = new(DescriptorType.StorageImage, DefaultImagePoolCapacity);
output[1] = new(DescriptorType.StorageTexelBuffer, DefaultImagePoolCapacity);
count = 2;
break;
}
return output[..count];
}
protected virtual unsafe void Dispose(bool disposing)
{
if (disposing)

View File

@@ -464,13 +464,14 @@ namespace Ryujinx.Graphics.Vulkan
return true;
}
public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(
VulkanRenderer gd,
int commandBufferIndex,
int setIndex,
out bool isNew)
public void UpdateDescriptorCacheCommandBufferIndex(int commandBufferIndex)
{
return _plce.GetNewDescriptorSetCollection(gd, commandBufferIndex, setIndex, out isNew);
_plce.UpdateCommandBufferIndex(commandBufferIndex);
}
public Auto<DescriptorSetCollection> GetNewDescriptorSetCollection(int setIndex, out bool isNew)
{
return _plce.GetNewDescriptorSetCollection(setIndex, out isNew);
}
protected virtual void Dispose(bool disposing)

View File

@@ -347,7 +347,7 @@ namespace Ryujinx.Graphics.Vulkan
CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex);
DescriptorSetManager = new DescriptorSetManager(_device);
DescriptorSetManager = new DescriptorSetManager(_device, PipelineBase.DescriptorSetLayouts);
PipelineLayoutCache = new PipelineLayoutCache();

View File

@@ -19,6 +19,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using Path = System.IO.Path;
namespace Ryujinx.HLE.FileSystem
@@ -817,13 +818,13 @@ namespace Ryujinx.HLE.FileSystem
if (updateNcas.Count > 0)
{
string extraNcas = string.Empty;
StringBuilder extraNcas = new();
foreach (var entry in updateNcas)
{
foreach (var (type, path) in entry.Value)
{
extraNcas += path + Environment.NewLine;
extraNcas.AppendLine(path);
}
}
@@ -954,13 +955,13 @@ namespace Ryujinx.HLE.FileSystem
if (updateNcas.Count > 0)
{
string extraNcas = string.Empty;
StringBuilder extraNcas = new();
foreach (var entry in updateNcas)
{
foreach (var (type, path) in entry.Value)
{
extraNcas += path + Environment.NewLine;
extraNcas.AppendLine(path);
}
}

View File

@@ -327,8 +327,10 @@ namespace Ryujinx.HLE.HOS
private void StartNewServices()
{
HorizonFsClient fsClient = new(this);
ServiceTable = new ServiceTable();
var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices, LibHacHorizonManager.BcatClient));
var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices, LibHacHorizonManager.BcatClient, fsClient));
foreach (var service in services)
{

View File

@@ -0,0 +1,119 @@
using LibHac.Common;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Horizon;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Fs;
using System;
using System.Collections.Concurrent;
using System.IO;
namespace Ryujinx.HLE.HOS
{
class HorizonFsClient : IFsClient
{
private readonly Horizon _system;
private readonly LibHac.Fs.FileSystemClient _fsClient;
private readonly ConcurrentDictionary<string, LocalStorage> _mountedStorages;
public HorizonFsClient(Horizon system)
{
_system = system;
_fsClient = _system.LibHacHorizonManager.FsClient.Fs;
_mountedStorages = new();
}
public void CloseFile(FileHandle handle)
{
_fsClient.CloseFile((LibHac.Fs.FileHandle)handle.Value);
}
public Result GetFileSize(out long size, FileHandle handle)
{
return _fsClient.GetFileSize(out size, (LibHac.Fs.FileHandle)handle.Value).ToHorizonResult();
}
public Result MountSystemData(string mountName, ulong dataId)
{
string contentPath = _system.ContentManager.GetInstalledContentPath(dataId, StorageId.BuiltInSystem, NcaContentType.PublicData);
string installPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
if (!string.IsNullOrWhiteSpace(installPath))
{
string ncaPath = installPath;
if (File.Exists(ncaPath))
{
LocalStorage ncaStorage = null;
try
{
ncaStorage = new LocalStorage(ncaPath, FileAccess.Read, FileMode.Open);
Nca nca = new(_system.KeySet, ncaStorage);
using var ncaFileSystem = nca.OpenFileSystem(NcaSectionType.Data, _system.FsIntegrityCheckLevel);
using var ncaFsRef = new UniqueRef<IFileSystem>(ncaFileSystem);
Result result = _fsClient.Register(mountName.ToU8Span(), ref ncaFsRef.Ref).ToHorizonResult();
if (result.IsFailure)
{
ncaStorage.Dispose();
}
else
{
_mountedStorages.TryAdd(mountName, ncaStorage);
}
return result;
}
catch (HorizonResultException ex)
{
ncaStorage?.Dispose();
return ex.ResultValue.ToHorizonResult();
}
}
}
// TODO: Return correct result here, this is likely wrong.
return LibHac.Fs.ResultFs.TargetNotFound.Handle().ToHorizonResult();
}
public Result OpenFile(out FileHandle handle, string path, OpenMode openMode)
{
var result = _fsClient.OpenFile(out var libhacHandle, path.ToU8Span(), (LibHac.Fs.OpenMode)openMode);
handle = new(libhacHandle);
return result.ToHorizonResult();
}
public Result QueryMountSystemDataCacheSize(out long size, ulong dataId)
{
// TODO.
size = 0;
return Result.Success;
}
public Result ReadFile(FileHandle handle, long offset, Span<byte> destination)
{
return _fsClient.ReadFile((LibHac.Fs.FileHandle)handle.Value, offset, destination).ToHorizonResult();
}
public void Unmount(string mountName)
{
if (_mountedStorages.TryRemove(mountName, out LocalStorage ncaStorage))
{
ncaStorage.Dispose();
}
_fsClient.Unmount(mountName.ToU8Span());
}
}
}

View File

@@ -436,14 +436,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
uint nameIndex = sym.NameOffset;
string name = string.Empty;
StringBuilder nameBuilder = new();
for (int chr; (chr = memory.Read<byte>(strTblAddr + nameIndex++)) != 0;)
{
name += (char)chr;
nameBuilder.Append((char)chr);
}
return new ElfSymbol(name, sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
return new ElfSymbol(nameBuilder.ToString(), sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
}
private static ElfSymbol GetSymbol32(IVirtualMemoryManager memory, ulong address, ulong strTblAddr)
@@ -452,14 +452,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
uint nameIndex = sym.NameOffset;
string name = string.Empty;
StringBuilder nameBuilder = new();
for (int chr; (chr = memory.Read<byte>(strTblAddr + nameIndex++)) != 0;)
{
name += (char)chr;
nameBuilder.Append((char)chr);
}
return new ElfSymbol(name, sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
return new ElfSymbol(nameBuilder.ToString(), sym.Info, sym.Other, sym.SectionIndex, sym.ValueAddress, sym.Size);
}
}
}

View File

@@ -36,6 +36,8 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
throw new InvalidOperationException("Out of handles!");
}
_completionEvent.WritableEvent.Signal();
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(completionEventHandle);
return ResultCode.Success;
@@ -187,6 +189,20 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
return ResultCode.Success;
}
[CommandCmif(10420)]
// nn::friends::CheckBlockedUserListAvailability(nn::account::Uid userId) -> bool
public ResultCode CheckBlockedUserListAvailability(ServiceCtx context)
{
UserId userId = context.RequestData.ReadStruct<UserId>();
// Yes, it is available.
context.ResponseData.Write(true);
Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = userId.ToString() });
return ResultCode.Success;
}
[CommandCmif(10600)]
// nn::friends::DeclareOpenOnlinePlaySession(nn::account::Uid userId)
public ResultCode DeclareOpenOnlinePlaySession(ServiceCtx context)

View File

@@ -29,6 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
private const bool IsDevelopment = false;
private readonly KEvent _stateChangeEvent;
private int _stateChangeEventHandle;
private NetworkState _state;
private DisconnectReason _disconnectReason;
@@ -277,12 +278,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
// AttachStateChangeEvent() -> handle<copy>
public ResultCode AttachStateChangeEvent(ServiceCtx context)
{
if (context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out int stateChangeEventHandle) != Result.Success)
if (_stateChangeEventHandle == 0 && context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(stateChangeEventHandle);
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangeEventHandle);
// Returns ResultCode.InvalidArgument if handle is null, doesn't occur in our case since we already throw an Exception.
@@ -964,6 +965,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
SetDisconnectReason(DisconnectReason.None);
}
if (_stateChangeEventHandle != 0)
{
context.Process.HandleTable.CloseHandle(_stateChangeEventHandle);
_stateChangeEventHandle = 0;
}
return resultCode;
}
@@ -1021,7 +1028,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
SetState(NetworkState.None);
NetworkClient?.DisconnectAndStop();
NetworkClient?.Dispose();
NetworkClient = null;
return ResultCode.Success;
@@ -1072,7 +1079,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
else
{
// NOTE: Service returns differents ResultCode here related to the nifm ResultCode.
// NOTE: Service returns different ResultCode here related to the nifm ResultCode.
resultCode = ResultCode.DeviceDisabled;
_nifmResultCode = resultCode;
}
@@ -1084,14 +1091,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public void Dispose()
{
if (NetworkClient != null)
{
_station?.Dispose();
_accessPoint?.Dispose();
_station?.Dispose();
_station = null;
NetworkClient.DisconnectAndStop();
}
_accessPoint?.Dispose();
_accessPoint = null;
NetworkClient?.Dispose();
NetworkClient = null;
}
}

View File

@@ -290,7 +290,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii
{
coreData = new CoreData();
if (charInfo.IsValid())
if (!charInfo.IsValid())
{
return ResultCode.InvalidCharInfo;
}

View File

@@ -1,4 +1,5 @@
using System;
using Ryujinx.Common.Memory;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Ryujinx.HLE.HOS.Services.Mii.Types.RandomMiiConstants;
@@ -10,9 +11,9 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
{
public const int Size = 0x30;
private byte _storage;
private Array48<byte> _storage;
public Span<byte> Storage => MemoryMarshal.CreateSpan(ref _storage, Size);
public Span<byte> Storage => _storage.AsSpan();
[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 0x18)]
public struct ElementInfo

View File

@@ -1,4 +1,5 @@
using System;
using Ryujinx.Common.Memory;
using System;
using System.Runtime.InteropServices;
using System.Text;
@@ -10,12 +11,12 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
public const int CharCount = 10;
private const int SizeConst = (CharCount + 1) * 2;
private byte _storage;
private Array22<byte> _storage;
public static Nickname Default => FromString("no name");
public static Nickname Question => FromString("???");
public Span<byte> Raw => MemoryMarshal.CreateSpan(ref _storage, SizeConst);
public Span<byte> Raw => _storage.AsSpan();
private ReadOnlySpan<ushort> Characters => MemoryMarshal.Cast<byte, ushort>(Raw);

View File

@@ -62,7 +62,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
private ushort CalculateDataCrc()
{
return Helper.CalculateCrc16(AsSpanWithoutDeviceCrc(), 0, true);
return Helper.CalculateCrc16(AsSpanWithoutCrcs(), 0, true);
}
private ushort CalculateDeviceCrc()
@@ -71,7 +71,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
ushort deviceIdCrc16 = Helper.CalculateCrc16(SpanHelpers.AsByteSpan(ref deviceId), 0, false);
return Helper.CalculateCrc16(AsSpan(), deviceIdCrc16, true);
return Helper.CalculateCrc16(AsSpanWithoutDeviceCrc(), deviceIdCrc16, true);
}
private ReadOnlySpan<byte> AsSpan()
@@ -84,6 +84,11 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
return AsSpan()[..(Size - 2)];
}
private ReadOnlySpan<byte> AsSpanWithoutCrcs()
{
return AsSpan()[..(Size - 4)];
}
public static StoreData BuildDefault(UtilityImpl utilImpl, uint index)
{
StoreData result = new()

View File

@@ -1,4 +1,6 @@
using System;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Mii.Types
@@ -8,9 +10,9 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
{
public const int Size = 0x60;
private byte _storage;
private Array96<byte> _storage;
public Span<byte> Storage => MemoryMarshal.CreateSpan(ref _storage, Size);
public Span<byte> Storage => _storage.AsSpan();
// TODO: define all getters/setters
}

View File

@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace Ryujinx.HLE.HOS.Services.Sm
{
@@ -235,7 +236,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm
private static string ReadName(ServiceCtx context)
{
string name = string.Empty;
StringBuilder nameBuilder = new();
for (int index = 0; index < 8 &&
context.RequestData.BaseStream.Position <
@@ -245,11 +246,11 @@ namespace Ryujinx.HLE.HOS.Services.Sm
if (chr >= 0x20 && chr < 0x7f)
{
name += (char)chr;
nameBuilder.Append((char)chr);
}
}
return name;
return nameBuilder.ToString();
}
public override void DestroyAtExit()

View File

@@ -669,6 +669,12 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
lock (Core.Lock)
{
// If we are replacing a buffer that has already been queued, make sure we release the references.
if (Core.Slots[slot].BufferState == BufferState.Queued)
{
Core.Slots[slot].GraphicBuffer.Object.DecrementNvMapHandleRefCount(Core.Owner);
}
Core.Slots[slot].BufferState = BufferState.Free;
Core.Slots[slot].Fence = AndroidFence.NoFence;
Core.Slots[slot].RequestBufferCalled = false;

View File

@@ -142,7 +142,7 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
// OpenDisplay(nn::vi::DisplayName) -> u64 display_id
public ResultCode OpenDisplay(ServiceCtx context)
{
string name = "";
StringBuilder nameBuilder = new();
for (int index = 0; index < 8 && context.RequestData.BaseStream.Position < context.RequestData.BaseStream.Length; index++)
{
@@ -150,11 +150,11 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService
if (chr >= 0x20 && chr < 0x7f)
{
name += (char)chr;
nameBuilder.Append((char)chr);
}
}
return OpenDisplayImpl(context, name);
return OpenDisplayImpl(context, nameBuilder.ToString());
}
[CommandCmif(1011)]

View File

@@ -1,4 +1,5 @@
using LibHac;
using Ryujinx.Horizon.Sdk.Fs;
namespace Ryujinx.Horizon
{
@@ -8,12 +9,14 @@ namespace Ryujinx.Horizon
public bool ThrowOnInvalidCommandIds { get; }
public HorizonClient BcatClient { get; }
public IFsClient FsClient { get; }
public HorizonOptions(bool ignoreMissingServices, HorizonClient bcatClient)
public HorizonOptions(bool ignoreMissingServices, HorizonClient bcatClient, IFsClient fsClient)
{
IgnoreMissingServices = ignoreMissingServices;
ThrowOnInvalidCommandIds = true;
BcatClient = bcatClient;
FsClient = fsClient;
}
}
}

View File

@@ -2,7 +2,7 @@
namespace Ryujinx.Horizon
{
internal static class LibHacResultExtensions
public static class LibHacResultExtensions
{
public static Result ToHorizonResult(this LibHac.Result result)
{

View File

@@ -11,7 +11,7 @@ namespace Ryujinx.Horizon.LogManager.Ipc
[CmifCommand(0)]
public Result OpenLogger(out LmLogger logger, [ClientProcessId] ulong pid)
{
// NOTE: Internal name is Logger, but we rename it LmLogger to avoid name clash with Ryujinx.Common.Logging logger.
// NOTE: Internal name is Logger, but we rename it to LmLogger to avoid name clash with Ryujinx.Common.Logging logger.
logger = new LmLogger(this, pid);
return Result.Success;

View File

@@ -0,0 +1,64 @@
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Ngc;
using Ryujinx.Horizon.Sdk.Ngc.Detail;
using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System;
namespace Ryujinx.Horizon.Ngc.Ipc
{
partial class Service : INgcService
{
private readonly ProfanityFilter _profanityFilter;
public Service(ProfanityFilter profanityFilter)
{
_profanityFilter = profanityFilter;
}
[CmifCommand(0)]
public Result GetContentVersion(out uint version)
{
lock (_profanityFilter)
{
return _profanityFilter.GetContentVersion(out version);
}
}
[CmifCommand(1)]
public Result Check(out uint checkMask, ReadOnlySpan<byte> text, uint regionMask, ProfanityFilterOption option)
{
lock (_profanityFilter)
{
return _profanityFilter.CheckProfanityWords(out checkMask, text, regionMask, option);
}
}
[CmifCommand(2)]
public Result Mask(
out int maskedWordsCount,
[Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> filteredText,
[Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> text,
uint regionMask,
ProfanityFilterOption option)
{
lock (_profanityFilter)
{
int length = Math.Min(filteredText.Length, text.Length);
text[..length].CopyTo(filteredText[..length]);
return _profanityFilter.MaskProfanityWordsInText(out maskedWordsCount, filteredText, regionMask, option);
}
}
[CmifCommand(3)]
public Result Reload()
{
lock (_profanityFilter)
{
return _profanityFilter.Reload();
}
}
}
}

View File

@@ -0,0 +1,51 @@
using Ryujinx.Horizon.Ngc.Ipc;
using Ryujinx.Horizon.Sdk.Fs;
using Ryujinx.Horizon.Sdk.Ngc.Detail;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using Ryujinx.Horizon.Sdk.Sm;
using System;
namespace Ryujinx.Horizon.Ngc
{
class NgcIpcServer
{
private const int MaxSessionsCount = 4;
private const int PointerBufferSize = 0;
private const int MaxDomains = 0;
private const int MaxDomainObjects = 0;
private const int MaxPortsCount = 1;
private static readonly ManagerOptions _options = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
private SmApi _sm;
private ServerManager _serverManager;
private ProfanityFilter _profanityFilter;
public void Initialize(IFsClient fsClient)
{
HeapAllocator allocator = new();
_sm = new SmApi();
_sm.Initialize().AbortOnFailure();
_profanityFilter = new(fsClient);
_profanityFilter.Initialize().AbortOnFailure();
_serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _options, MaxSessionsCount);
_serverManager.RegisterObjectForServer(new Service(_profanityFilter), ServiceName.Encode("ngc:u"), MaxSessionsCount);
}
public void ServiceRequests()
{
_serverManager.ServiceRequests();
}
public void Shutdown()
{
_serverManager.Dispose();
_profanityFilter.Dispose();
}
}
}

View File

@@ -0,0 +1,21 @@
namespace Ryujinx.Horizon.Ngc
{
class NgcMain : IService
{
public static void Main(ServiceTable serviceTable)
{
NgcIpcServer ipcServer = new();
ipcServer.Initialize(HorizonStatic.Options.FsClient);
// TODO: Notification thread, requires implementing OpenSystemDataUpdateEventNotifier on FS.
// The notification thread seems to wait until the event returned by OpenSystemDataUpdateEventNotifier is signalled
// in a loop. When it receives the signal, it calls ContentsReader.Reload and then waits for the next signal.
serviceTable.SignalServiceReady();
ipcServer.ServiceRequests();
ipcServer.Shutdown();
}
}
}

View File

@@ -0,0 +1,13 @@
namespace Ryujinx.Horizon.Sdk.Fs
{
public readonly struct FileHandle
{
public object Value { get; }
public FileHandle(object value)
{
Value = value;
}
}
}

View File

@@ -0,0 +1,13 @@
using Ryujinx.Horizon.Common;
namespace Ryujinx.Horizon.Sdk.Fs
{
static class FsResult
{
private const int ModuleId = 2;
public static Result PathNotFound => new(ModuleId, 1);
public static Result PathAlreadyExists => new(ModuleId, 2);
public static Result TargetNotFound => new(ModuleId, 1002);
}
}

View File

@@ -0,0 +1,16 @@
using Ryujinx.Horizon.Common;
using System;
namespace Ryujinx.Horizon.Sdk.Fs
{
public interface IFsClient
{
Result QueryMountSystemDataCacheSize(out long size, ulong dataId);
Result MountSystemData(string mountName, ulong dataId);
Result OpenFile(out FileHandle handle, string path, OpenMode openMode);
Result ReadFile(FileHandle handle, long offset, Span<byte> destination);
Result GetFileSize(out long size, FileHandle handle);
void CloseFile(FileHandle handle);
void Unmount(string mountName);
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Ryujinx.Horizon.Sdk.Fs
{
[Flags]
public enum OpenMode
{
Read = 1,
Write = 2,
AllowAppend = 4,
ReadWrite = 3,
All = 7,
}
}

View File

@@ -0,0 +1,251 @@
using System;
using System.Diagnostics;
using System.Text;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
class AhoCorasick
{
public delegate bool MatchCallback(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state);
public delegate bool MatchCallback<T>(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref T state);
private readonly SparseSet _wordMap = new();
private readonly CompressedArray _wordLengths = new();
private readonly SparseSet _multiWordMap = new();
private readonly CompressedArray _multiWordIndices = new();
private readonly SparseSet _nodeMap = new();
private uint _nodesPerCharacter;
private readonly Bp _bp = new();
public bool Import(ref BinaryReader reader)
{
if (!_wordLengths.Import(ref reader) ||
!_wordMap.Import(ref reader) ||
!_multiWordIndices.Import(ref reader) ||
!_multiWordMap.Import(ref reader))
{
return false;
}
if (!reader.Read(out _nodesPerCharacter))
{
return false;
}
return _nodeMap.Import(ref reader) && _bp.Import(ref reader);
}
public void Match(ReadOnlySpan<byte> utf8Text, MatchCallback callback, ref MatchState state)
{
int nodeId = 0;
for (int index = 0; index < utf8Text.Length; index++)
{
long c = utf8Text[index];
while (true)
{
long nodeSparseIndex = _nodesPerCharacter * c + (uint)nodeId;
int nodePlainIndex = _nodeMap.Rank1(nodeSparseIndex);
if (nodePlainIndex != 0)
{
long foundNodeSparseIndex = _nodeMap.Select1Ex(nodePlainIndex - 1);
if (foundNodeSparseIndex > 0 && foundNodeSparseIndex == nodeSparseIndex)
{
nodeId = nodePlainIndex;
if (callback != null)
{
// Match full word.
if (_wordMap.Has(nodePlainIndex))
{
int wordLength = _wordLengths[_wordMap.Rank1((uint)nodePlainIndex) - 1];
int startIndex = index + 1 - wordLength;
if (!callback(utf8Text, startIndex, index + 1, nodeId, ref state))
{
return;
}
}
// If this is a phrase composed of multiple words, also match each sub-word.
while (_multiWordMap.Has(nodePlainIndex))
{
nodePlainIndex = _multiWordIndices[_multiWordMap.Rank1((uint)nodePlainIndex) - 1];
int wordLength = _wordMap.Has(nodePlainIndex) ? _wordLengths[_wordMap.Rank1(nodePlainIndex) - 1] : 0;
int startIndex = index + 1 - wordLength;
if (!callback(utf8Text, startIndex, index + 1, nodePlainIndex, ref state))
{
return;
}
}
}
break;
}
}
if (nodeId == 0)
{
break;
}
int nodePos = _bp.ToPos(nodeId);
nodePos = _bp.Enclose(nodePos);
if (nodePos < 0)
{
return;
}
nodeId = _bp.ToNodeId(nodePos);
}
}
}
public void Match<T>(ReadOnlySpan<byte> utf8Text, MatchCallback<T> callback, ref T state)
{
int nodeId = 0;
for (int index = 0; index < utf8Text.Length; index++)
{
long c = utf8Text[index];
while (true)
{
long nodeSparseIndex = _nodesPerCharacter * c + (uint)nodeId;
int nodePlainIndex = _nodeMap.Rank1(nodeSparseIndex);
if (nodePlainIndex != 0)
{
long foundNodeSparseIndex = _nodeMap.Select1Ex(nodePlainIndex - 1);
if (foundNodeSparseIndex > 0 && foundNodeSparseIndex == nodeSparseIndex)
{
nodeId = nodePlainIndex;
if (callback != null)
{
// Match full word.
if (_wordMap.Has(nodePlainIndex))
{
int wordLength = _wordLengths[_wordMap.Rank1((uint)nodePlainIndex) - 1];
int startIndex = index + 1 - wordLength;
if (!callback(utf8Text, startIndex, index + 1, nodeId, ref state))
{
return;
}
}
// If this is a phrase composed of multiple words, also match each sub-word.
while (_multiWordMap.Has(nodePlainIndex))
{
nodePlainIndex = _multiWordIndices[_multiWordMap.Rank1((uint)nodePlainIndex) - 1];
int wordLength = _wordMap.Has(nodePlainIndex) ? _wordLengths[_wordMap.Rank1(nodePlainIndex) - 1] : 0;
int startIndex = index + 1 - wordLength;
if (!callback(utf8Text, startIndex, index + 1, nodePlainIndex, ref state))
{
return;
}
}
}
break;
}
}
if (nodeId == 0)
{
break;
}
int nodePos = _bp.ToPos(nodeId);
nodePos = _bp.Enclose(nodePos);
if (nodePos < 0)
{
return;
}
nodeId = _bp.ToNodeId(nodePos);
}
}
}
public string GetWordList(bool includeMultiWord = true)
{
// Storage must be large enough to fit the largest word in the dictionary.
// Since this is only used for debugging, it's fine to increase the size manually if needed.
StringBuilder sb = new();
Span<byte> storage = new byte[1024];
// Traverse trie from the root.
GetWord(sb, storage, 0, 0, includeMultiWord);
return sb.ToString();
}
private void GetWord(StringBuilder sb, Span<byte> storage, int storageOffset, int nodeId, bool includeMultiWord)
{
int characters = (int)((_nodeMap.RangeEndValue + _nodesPerCharacter - 1) / _nodesPerCharacter);
for (int c = 0; c < characters; c++)
{
long nodeSparseIndex = _nodesPerCharacter * c + (uint)nodeId;
int nodePlainIndex = _nodeMap.Rank1(nodeSparseIndex);
if (nodePlainIndex != 0)
{
long foundNodeSparseIndex = _nodeMap.Select1Ex(nodePlainIndex - 1);
if (foundNodeSparseIndex > 0 && foundNodeSparseIndex == nodeSparseIndex)
{
storage[storageOffset] = (byte)c;
int nextNodeId = nodePlainIndex;
if (_wordMap.Has(nodePlainIndex))
{
sb.AppendLine(Encoding.UTF8.GetString(storage[..(storageOffset + 1)]));
// Some basic validation to ensure we imported the dictionary properly.
int wordLength = _wordLengths[_wordMap.Rank1((uint)nodePlainIndex) - 1];
Debug.Assert(storageOffset + 1 == wordLength);
}
if (includeMultiWord)
{
int lastMultiWordIndex = 0;
string multiWord = "";
while (_multiWordMap.Has(nodePlainIndex))
{
nodePlainIndex = _multiWordIndices[_multiWordMap.Rank1((uint)nodePlainIndex) - 1];
int wordLength = _wordMap.Has(nodePlainIndex) ? _wordLengths[_wordMap.Rank1(nodePlainIndex) - 1] : 0;
int startIndex = storageOffset + 1 - wordLength;
multiWord += Encoding.UTF8.GetString(storage[lastMultiWordIndex..startIndex]) + " ";
lastMultiWordIndex = startIndex;
}
if (lastMultiWordIndex != 0)
{
multiWord += Encoding.UTF8.GetString(storage[lastMultiWordIndex..(storageOffset + 1)]);
sb.AppendLine(multiWord);
}
}
GetWord(sb, storage, storageOffset + 1, nextNodeId, includeMultiWord);
}
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
ref struct BinaryReader
{
private readonly ReadOnlySpan<byte> _data;
private int _offset;
public BinaryReader(ReadOnlySpan<byte> data)
{
_data = data;
}
public bool Read<T>(out T value) where T : unmanaged
{
int byteLength = Unsafe.SizeOf<T>();
if ((uint)(_offset + byteLength) <= (uint)_data.Length)
{
value = MemoryMarshal.Cast<byte, T>(_data[_offset..])[0];
_offset += byteLength;
return true;
}
value = default;
return false;
}
public int AllocateAndReadArray<T>(ref T[] array, int length, int maxLengthExclusive) where T : unmanaged
{
return AllocateAndReadArray(ref array, Math.Min(length, maxLengthExclusive));
}
public int AllocateAndReadArray<T>(ref T[] array, int length) where T : unmanaged
{
array = new T[length];
return ReadArray(array);
}
public int ReadArray<T>(T[] array) where T : unmanaged
{
if (array != null)
{
int byteLength = array.Length * Unsafe.SizeOf<T>();
byteLength = Math.Min(byteLength, _data.Length - _offset);
MemoryMarshal.Cast<byte, T>(_data.Slice(_offset, byteLength)).CopyTo(array);
_offset += byteLength;
return byteLength / Unsafe.SizeOf<T>();
}
return 0;
}
}
}

View File

@@ -0,0 +1,78 @@
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
class BitVector32
{
private const int BitsPerWord = Set.BitsPerWord;
private int _bitLength;
private uint[] _array;
public int BitLength => _bitLength;
public uint[] Array => _array;
public BitVector32()
{
_bitLength = 0;
_array = null;
}
public BitVector32(int length)
{
_bitLength = length;
_array = new uint[(length + BitsPerWord - 1) / BitsPerWord];
}
public bool Has(int index)
{
if ((uint)index < (uint)_bitLength)
{
int wordIndex = index / BitsPerWord;
int wordBitOffset = index % BitsPerWord;
return ((_array[wordIndex] >> wordBitOffset) & 1u) != 0;
}
return false;
}
public bool TurnOn(int index, int count)
{
for (int bit = 0; bit < count; bit++)
{
if (!TurnOn(index + bit))
{
return false;
}
}
return true;
}
public bool TurnOn(int index)
{
if ((uint)index < (uint)_bitLength)
{
int wordIndex = index / BitsPerWord;
int wordBitOffset = index % BitsPerWord;
_array[wordIndex] |= 1u << wordBitOffset;
return true;
}
return false;
}
public bool Import(ref BinaryReader reader)
{
if (!reader.Read(out _bitLength))
{
return false;
}
int arrayLength = (_bitLength + BitsPerWord - 1) / BitsPerWord;
return reader.AllocateAndReadArray(ref _array, arrayLength) == arrayLength;
}
}
}

View File

@@ -0,0 +1,54 @@
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
class Bp
{
private readonly BpNode _firstNode = new();
private readonly SbvSelect _sbvSelect = new();
public bool Import(ref BinaryReader reader)
{
return _firstNode.Import(ref reader) && _sbvSelect.Import(ref reader);
}
public int ToPos(int index)
{
return _sbvSelect.Select(_firstNode.Set, index);
}
public int Enclose(int index)
{
if ((uint)index < (uint)_firstNode.Set.BitVector.BitLength)
{
if (!_firstNode.Set.Has(index))
{
index = _firstNode.FindOpen(index);
}
if (index > 0)
{
return _firstNode.Enclose(index);
}
}
return -1;
}
public int ToNodeId(int index)
{
if ((uint)index < (uint)_firstNode.Set.BitVector.BitLength)
{
if (!_firstNode.Set.Has(index))
{
index = _firstNode.FindOpen(index);
}
if (index >= 0)
{
return _firstNode.Set.Rank1(index) - 1;
}
}
return -1;
}
}
}

View File

@@ -0,0 +1,241 @@
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
class BpNode
{
private readonly Set _set = new();
private SparseSet _sparseSet;
private BpNode _nextNode;
public Set Set => _set;
public bool Import(ref BinaryReader reader)
{
if (!_set.Import(ref reader))
{
return false;
}
if (!reader.Read(out byte hasNext))
{
return false;
}
if (hasNext == 0)
{
return true;
}
_sparseSet = new();
_nextNode = new();
return _sparseSet.Import(ref reader) && _nextNode.Import(ref reader);
}
public int FindOpen(int index)
{
uint membershipBits = _set.BitVector.Array[index / Set.BitsPerWord];
int wordBitOffset = index % Set.BitsPerWord;
int unsetBits = 1;
for (int bit = wordBitOffset - 1; bit >= 0; bit--)
{
if (((membershipBits >> bit) & 1) != 0)
{
if (--unsetBits == 0)
{
return (index & ~(Set.BitsPerWord - 1)) | bit;
}
}
else
{
unsetBits++;
}
}
int plainIndex = _sparseSet.Rank1(index);
if (plainIndex == 0)
{
return -1;
}
int newIndex = index;
if (!_sparseSet.Has(index))
{
if (plainIndex == 0 || _nextNode == null)
{
return -1;
}
newIndex = _sparseSet.Select1(plainIndex);
if (newIndex < 0)
{
return -1;
}
}
else
{
plainIndex--;
}
int openIndex = _nextNode.FindOpen(plainIndex);
if (openIndex < 0)
{
return -1;
}
int openSparseIndex = _sparseSet.Select1(openIndex);
if (openSparseIndex < 0)
{
return -1;
}
if (newIndex != index)
{
unsetBits = 1;
for (int bit = newIndex % Set.BitsPerWord - 1; bit > wordBitOffset; bit--)
{
unsetBits += ((membershipBits >> bit) & 1) != 0 ? -1 : 1;
}
int bestCandidate = -1;
membershipBits = _set.BitVector.Array[openSparseIndex / Set.BitsPerWord];
for (int bit = openSparseIndex % Set.BitsPerWord + 1; bit < Set.BitsPerWord; bit++)
{
if (unsetBits - 1 == 0)
{
bestCandidate = bit;
}
unsetBits += ((membershipBits >> bit) & 1) != 0 ? -1 : 1;
}
return (openSparseIndex & ~(Set.BitsPerWord - 1)) | bestCandidate;
}
else
{
return openSparseIndex;
}
}
public int Enclose(int index)
{
uint membershipBits = _set.BitVector.Array[index / Set.BitsPerWord];
int unsetBits = 1;
for (int bit = index % Set.BitsPerWord - 1; bit >= 0; bit--)
{
if (((membershipBits >> bit) & 1) != 0)
{
if (--unsetBits == 0)
{
return (index & ~(Set.BitsPerWord - 1)) + bit;
}
}
else
{
unsetBits++;
}
}
int setBits = 2;
for (int bit = index % Set.BitsPerWord + 1; bit < Set.BitsPerWord; bit++)
{
if (((membershipBits >> bit) & 1) != 0)
{
setBits++;
}
else
{
if (--setBits == 0)
{
return FindOpen((index & ~(Set.BitsPerWord - 1)) + bit);
}
}
}
int newIndex = index;
if (!_sparseSet.Has(index))
{
newIndex = _sparseSet.Select1(_sparseSet.Rank1(index));
if (newIndex < 0)
{
return -1;
}
}
if (!_set.Has(newIndex))
{
newIndex = FindOpen(newIndex);
if (newIndex < 0)
{
return -1;
}
}
else
{
newIndex = _nextNode.Enclose(_sparseSet.Rank1(newIndex) - 1);
if (newIndex < 0)
{
return -1;
}
newIndex = _sparseSet.Select1(newIndex);
}
int nearestIndex = _sparseSet.Select1(_sparseSet.Rank1(newIndex));
if (nearestIndex < 0)
{
return -1;
}
setBits = 0;
membershipBits = _set.BitVector.Array[newIndex / Set.BitsPerWord];
if ((newIndex / Set.BitsPerWord) == (nearestIndex / Set.BitsPerWord))
{
for (int bit = nearestIndex % Set.BitsPerWord - 1; bit >= newIndex % Set.BitsPerWord; bit--)
{
if (((membershipBits >> bit) & 1) != 0)
{
if (++setBits > 0)
{
return (newIndex & ~(Set.BitsPerWord - 1)) + bit;
}
}
else
{
setBits--;
}
}
}
else
{
for (int bit = Set.BitsPerWord - 1; bit >= newIndex % Set.BitsPerWord; bit--)
{
if (((membershipBits >> bit) & 1) != 0)
{
if (++setBits > 0)
{
return (newIndex & ~(Set.BitsPerWord - 1)) + bit;
}
}
else
{
setBits--;
}
}
}
return -1;
}
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
class CompressedArray
{
private const int MaxUncompressedEntries = 64;
private const int CompressedEntriesPerBlock = 64;
private const int BitsPerWord = Set.BitsPerWord;
private readonly struct BitfieldRange
{
private readonly uint _range;
private readonly int _baseValue;
public int BitfieldIndex => (int)(_range & 0x7ffffff);
public int BitfieldLength => (int)(_range >> 27) + 1;
public int BaseValue => _baseValue;
public BitfieldRange(uint range, int baseValue)
{
_range = range;
_baseValue = baseValue;
}
}
private uint[] _bitfieldRanges;
private uint[] _bitfields;
private int[] _uncompressedArray;
public int Length => (_bitfieldRanges.Length / 2) * CompressedEntriesPerBlock + _uncompressedArray.Length;
public int this[int index]
{
get
{
var ranges = GetBitfieldRanges();
int rangeBlockIndex = index / CompressedEntriesPerBlock;
if (rangeBlockIndex < ranges.Length)
{
var range = ranges[rangeBlockIndex];
int bitfieldLength = range.BitfieldLength;
int bitfieldOffset = (index % CompressedEntriesPerBlock) * bitfieldLength;
int bitfieldIndex = range.BitfieldIndex + (bitfieldOffset / BitsPerWord);
int bitOffset = bitfieldOffset % BitsPerWord;
ulong bitfieldValue = _bitfields[bitfieldIndex];
// If the bit fields crosses the word boundary, let's load the next one to ensure we
// have access to the full value.
if (bitOffset + bitfieldLength > BitsPerWord)
{
bitfieldValue |= (ulong)_bitfields[bitfieldIndex + 1] << 32;
}
int value = (int)(bitfieldValue >> bitOffset) & ((1 << bitfieldLength) - 1);
// Sign-extend.
int remainderBits = BitsPerWord - bitfieldLength;
value <<= remainderBits;
value >>= remainderBits;
return value + range.BaseValue;
}
else if (rangeBlockIndex < _uncompressedArray.Length + _bitfieldRanges.Length * BitsPerWord)
{
return _uncompressedArray[index % MaxUncompressedEntries];
}
return 0;
}
}
private ReadOnlySpan<BitfieldRange> GetBitfieldRanges()
{
return MemoryMarshal.Cast<uint, BitfieldRange>(_bitfieldRanges);
}
public bool Import(ref BinaryReader reader)
{
if (!reader.Read(out int bitfieldRangesCount) ||
reader.AllocateAndReadArray(ref _bitfieldRanges, bitfieldRangesCount) != bitfieldRangesCount)
{
return false;
}
if (!reader.Read(out int bitfieldsCount) || reader.AllocateAndReadArray(ref _bitfields, bitfieldsCount) != bitfieldsCount)
{
return false;
}
return reader.Read(out byte uncompressedArrayLength) &&
reader.AllocateAndReadArray(ref _uncompressedArray, uncompressedArrayLength, MaxUncompressedEntries) == uncompressedArrayLength;
}
}
}

View File

@@ -0,0 +1,404 @@
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Fs;
using System;
using System.IO;
using System.IO.Compression;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
class ContentsReader : IDisposable
{
private const string MountName = "NgWord";
private const string VersionFilePath = $"{MountName}:/version.dat";
private const ulong DataId = 0x100000000000823UL;
private enum AcType
{
AcNotB,
AcB1,
AcB2,
AcSimilarForm,
TableSimilarForm,
}
private readonly IFsClient _fsClient;
private readonly object _lock;
private bool _intialized;
private ulong _cacheSize;
public ContentsReader(IFsClient fsClient)
{
_lock = new();
_fsClient = fsClient;
}
private static void MakeMountPoint(out string path, AcType type, int regionIndex)
{
path = null;
switch (type)
{
case AcType.AcNotB:
if (regionIndex < 0)
{
path = $"{MountName}:/ac_common_not_b_nx";
}
else
{
path = $"{MountName}:/ac_{regionIndex}_not_b_nx";
}
break;
case AcType.AcB1:
if (regionIndex < 0)
{
path = $"{MountName}:/ac_common_b1_nx";
}
else
{
path = $"{MountName}:/ac_{regionIndex}_b1_nx";
}
break;
case AcType.AcB2:
if (regionIndex < 0)
{
path = $"{MountName}:/ac_common_b2_nx";
}
else
{
path = $"{MountName}:/ac_{regionIndex}_b2_nx";
}
break;
case AcType.AcSimilarForm:
path = $"{MountName}:/ac_similar_form_nx";
break;
case AcType.TableSimilarForm:
path = $"{MountName}:/table_similar_form_nx";
break;
}
}
public Result Initialize(ulong cacheSize)
{
lock (_lock)
{
if (_intialized)
{
return Result.Success;
}
Result result = _fsClient.QueryMountSystemDataCacheSize(out long dataCacheSize, DataId);
if (result.IsFailure)
{
return result;
}
if (cacheSize < (ulong)dataCacheSize)
{
return NgcResult.InvalidSize;
}
result = _fsClient.MountSystemData(MountName, DataId);
if (result.IsFailure)
{
// Official firmware would return the result here,
// we don't to support older firmware where the archive didn't exist yet.
return Result.Success;
}
_cacheSize = cacheSize;
_intialized = true;
return Result.Success;
}
}
public Result Reload()
{
lock (_lock)
{
if (!_intialized)
{
return Result.Success;
}
_fsClient.Unmount(MountName);
Result result = Result.Success;
try
{
result = _fsClient.QueryMountSystemDataCacheSize(out long cacheSize, DataId);
if (result.IsFailure)
{
return result;
}
if (_cacheSize < (ulong)cacheSize)
{
result = NgcResult.InvalidSize;
return NgcResult.InvalidSize;
}
result = _fsClient.MountSystemData(MountName, DataId);
if (result.IsFailure)
{
return result;
}
}
finally
{
if (result.IsFailure)
{
_intialized = false;
_cacheSize = 0;
}
}
}
return Result.Success;
}
private Result GetFileSize(out long size, string filePath)
{
size = 0;
lock (_lock)
{
Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
if (result.IsFailure)
{
return result;
}
try
{
result = _fsClient.GetFileSize(out size, handle);
if (result.IsFailure)
{
return result;
}
}
finally
{
_fsClient.CloseFile(handle);
}
}
return Result.Success;
}
private Result GetFileContent(Span<byte> destination, string filePath)
{
lock (_lock)
{
Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
if (result.IsFailure)
{
return result;
}
try
{
result = _fsClient.ReadFile(handle, 0, destination);
if (result.IsFailure)
{
return result;
}
}
finally
{
_fsClient.CloseFile(handle);
}
}
return Result.Success;
}
public Result GetVersionDataSize(out long size)
{
return GetFileSize(out size, VersionFilePath);
}
public Result GetVersionData(Span<byte> destination)
{
return GetFileContent(destination, VersionFilePath);
}
public Result ReadDictionaries(out AhoCorasick partialWordsTrie, out AhoCorasick completeWordsTrie, out AhoCorasick delimitedWordsTrie, int regionIndex)
{
completeWordsTrie = null;
delimitedWordsTrie = null;
MakeMountPoint(out string partialWordsTriePath, AcType.AcNotB, regionIndex);
MakeMountPoint(out string completeWordsTriePath, AcType.AcB1, regionIndex);
MakeMountPoint(out string delimitedWordsTriePath, AcType.AcB2, regionIndex);
Result result = ReadDictionary(out partialWordsTrie, partialWordsTriePath);
if (result.IsFailure)
{
return NgcResult.DataAccessError;
}
result = ReadDictionary(out completeWordsTrie, completeWordsTriePath);
if (result.IsFailure)
{
return NgcResult.DataAccessError;
}
return ReadDictionary(out delimitedWordsTrie, delimitedWordsTriePath);
}
public Result ReadSimilarFormDictionary(out AhoCorasick similarFormTrie)
{
MakeMountPoint(out string similarFormTriePath, AcType.AcSimilarForm, 0);
return ReadDictionary(out similarFormTrie, similarFormTriePath);
}
public Result ReadSimilarFormTable(out SimilarFormTable similarFormTable)
{
similarFormTable = null;
MakeMountPoint(out string similarFormTablePath, AcType.TableSimilarForm, 0);
Result result = ReadGZipCompressedArchive(out byte[] data, similarFormTablePath);
if (result.IsFailure)
{
return result;
}
BinaryReader reader = new(data);
SimilarFormTable table = new();
if (!table.Import(ref reader))
{
// Official firmware doesn't return an error here and just assumes the import was successful.
return NgcResult.DataAccessError;
}
similarFormTable = table;
return Result.Success;
}
public static Result ReadNotSeparatorDictionary(out AhoCorasick notSeparatorTrie)
{
notSeparatorTrie = null;
BinaryReader reader = new(EmbeddedTries.NotSeparatorTrie);
AhoCorasick ac = new();
if (!ac.Import(ref reader))
{
// Official firmware doesn't return an error here and just assumes the import was successful.
return NgcResult.DataAccessError;
}
notSeparatorTrie = ac;
return Result.Success;
}
private Result ReadDictionary(out AhoCorasick trie, string path)
{
trie = null;
Result result = ReadGZipCompressedArchive(out byte[] data, path);
if (result.IsFailure)
{
return result;
}
BinaryReader reader = new(data);
AhoCorasick ac = new();
if (!ac.Import(ref reader))
{
// Official firmware doesn't return an error here and just assumes the import was successful.
return NgcResult.DataAccessError;
}
trie = ac;
return Result.Success;
}
private Result ReadGZipCompressedArchive(out byte[] data, string filePath)
{
data = null;
Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
if (result.IsFailure)
{
return result;
}
try
{
result = _fsClient.GetFileSize(out long fileSize, handle);
if (result.IsFailure)
{
return result;
}
data = new byte[fileSize];
result = _fsClient.ReadFile(handle, 0, data.AsSpan());
if (result.IsFailure)
{
return result;
}
}
finally
{
_fsClient.CloseFile(handle);
}
try
{
data = DecompressGZipCompressedStream(data);
}
catch (InvalidDataException)
{
// Official firmware returns a different error, but it is translated to this error on the caller.
return NgcResult.DataAccessError;
}
return Result.Success;
}
private static byte[] DecompressGZipCompressedStream(byte[] data)
{
using MemoryStream input = new(data);
using GZipStream gZipStream = new(input, CompressionMode.Decompress);
using MemoryStream output = new();
gZipStream.CopyTo(output);
return output.ToArray();
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_lock)
{
if (!_intialized)
{
return;
}
_fsClient.Unmount(MountName);
_intialized = false;
}
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,266 @@
using System;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
static class EmbeddedTries
{
public static ReadOnlySpan<byte> NotSeparatorTrie => new byte[]
{
0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00,
0xE9, 0xFF, 0xE9, 0xFF, 0xF4, 0xFF, 0xFA, 0xBF, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x5F, 0xFF, 0xAF,
0xFF, 0xEB, 0xFF, 0xFA, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0xFB, 0x7F, 0xFF, 0xEF, 0xFF, 0xFD,
0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0xF7, 0xFF, 0xE8, 0xFF, 0xE9, 0xFF, 0x00, 0x00, 0x00, 0x00,
0xFC, 0x3F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFA, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xE7, 0xFF,
0xFC, 0x9F, 0xFF, 0xF3, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x9F, 0xFF, 0xE7, 0xFF, 0xF9, 0x7F,
0x00, 0x00, 0x00, 0x00, 0xFE, 0x5F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00,
0x3F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xCF, 0xFF, 0xF3,
0xFF, 0xFC, 0x3F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xCF, 0xFF, 0xFB, 0x7F, 0xFE, 0x9F, 0xFF, 0xF3,
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x9F, 0xFF, 0xF3, 0xFF, 0xFC, 0x9F, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xF3, 0x7F, 0xFE, 0xCF, 0xFF, 0xF5, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0xA9, 0x52, 0x55, 0x55, 0xA9, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0xAA, 0x54, 0x55, 0xA5, 0x4A, 0x55, 0x55, 0x55, 0xAA, 0xAA, 0xAA, 0x52, 0x55, 0x55,
0x95, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0xA5, 0xAA, 0xAA, 0x2A, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xA5, 0xAA, 0xAA, 0xAA, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x4A,
0x55, 0x55, 0x55, 0xA9, 0xAA, 0xAA, 0x52, 0x55, 0x55, 0xA5, 0xAA, 0xAA, 0x4A, 0x55, 0x55, 0x05,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00,
0x00, 0xF7, 0x01, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, 0xF7, 0x02, 0x00, 0x00, 0x6E, 0x03, 0x00,
0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00,
0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00, 0x00, 0x6E, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x1F, 0x2F, 0x3F, 0x4E, 0x5E, 0x6D, 0x00, 0x0F, 0x1E,
0x2E, 0x3D, 0x4C, 0x5C, 0x6B, 0x00, 0x10, 0x20, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x00, 0x10, 0x20,
0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x3F, 0x4F, 0x5E, 0x6D, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x03,
0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x40, 0x00, 0x40, 0x00, 0x20,
0x00, 0x20, 0x00, 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0x04, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01,
0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00,
0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x40, 0x00, 0x40, 0x00, 0x20, 0x00, 0x10, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00,
0x21, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0D, 0x0F,
0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E,
0x00, 0x02, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x8A, 0x03, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0xC5, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x51, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x03, 0x00,
0x00, 0x07, 0x00, 0x00, 0x00, 0xC6, 0x00, 0x00, 0x00, 0xCF, 0xED, 0x81, 0x61, 0xD9, 0xDC, 0x8A,
0xD3, 0xF0, 0xBB, 0x05, 0x6E, 0xEB, 0x0D, 0x88, 0x6C, 0x39, 0x62, 0x01, 0x95, 0x82, 0xCF, 0xEE,
0x3A, 0x7F, 0x53, 0xDF, 0x09, 0x90, 0xF7, 0x06, 0xA4, 0x7A, 0x2D, 0xB3, 0xE7, 0xFA, 0x20, 0x48,
0x0F, 0x38, 0x34, 0xED, 0xBC, 0x8A, 0x96, 0xAB, 0x8E, 0xE3, 0xFF, 0xC6, 0xD2, 0xBF, 0xC0, 0x90,
0x06, 0x34, 0xDF, 0xF0, 0xDB, 0xDE, 0x27, 0x2E, 0xD5, 0x3C, 0xA2, 0x22, 0x72, 0xBD, 0x02, 0x0D,
0x1F, 0xB2, 0x99, 0xBE, 0x17, 0x26, 0xA1, 0xEF, 0x40, 0xF2, 0x61, 0xE1, 0x16, 0x17, 0xA4, 0xF4,
0x3A, 0x0F, 0x3C, 0x3A, 0xAB, 0x74, 0x83, 0x93, 0xB2, 0x09, 0x43, 0x52, 0x6E, 0xB8, 0xBF, 0xC8,
0x9C, 0x6A, 0x73, 0xD3, 0x0C, 0xC8, 0x5C, 0x71, 0xCD, 0x87, 0xCA, 0x28, 0xF6, 0xEB, 0x87, 0x60,
0x3D, 0xA5, 0x15, 0x9B, 0xAA, 0x99, 0x23, 0x9F, 0xD6, 0x2E, 0x79, 0x58, 0xE9, 0x8E, 0x54, 0xB0,
0xF8, 0x07, 0x6F, 0x6C, 0x52, 0xB7, 0xE2, 0x34, 0x42, 0x8C, 0x7A, 0xD5, 0xEC, 0xA4, 0xFE, 0x52,
0x9A, 0x05, 0x9F, 0xDD, 0x8D, 0x73, 0x8B, 0xA6, 0xDB, 0xA7, 0x84, 0xD0, 0xAB, 0xB7, 0xCC, 0x9E,
0x4B, 0xD8, 0xB2, 0xDC, 0x0F, 0xE8, 0x3A, 0x56, 0xB9, 0x63, 0x75, 0x1C, 0x7F, 0x89, 0xDF, 0x7C,
0x84, 0xE2, 0x8C, 0xA9, 0x0D, 0xA3, 0xDF, 0xF6, 0x3E, 0xC7, 0xCE, 0x1B, 0x24, 0x94, 0xB8, 0xE8,
0xD7, 0xDC, 0xA6, 0xEF, 0x85, 0xA1, 0x7D, 0x00, 0xE1, 0x78, 0xD4, 0x8B, 0x13, 0xCB, 0xB6, 0x4B,
0x5E, 0xCB, 0xF3, 0xC0, 0xA3, 0x09, 0x68, 0x68, 0x4C, 0xF4, 0x98, 0x0D, 0x38, 0x0D, 0xBF, 0xFB,
0x8B, 0xCC, 0x55, 0x71, 0x21, 0xC1, 0xFC, 0x3B, 0x60, 0x77, 0x9D, 0x3F, 0x54, 0x46, 0x61, 0x4A,
0xC8, 0xA5, 0xDB, 0x21, 0x8A, 0xCA, 0x73, 0x7D, 0x10, 0xF9, 0xB4, 0xD6, 0x9E, 0x15, 0x8E, 0x58,
0x94, 0x3C, 0xA9, 0xF1, 0x7F, 0x63, 0x93, 0xBA, 0xD5, 0x51, 0x35, 0xA1, 0x93, 0x93, 0xF5, 0xEE,
0x13, 0x97, 0xD2, 0x2C, 0xF8, 0x97, 0xFD, 0x98, 0x58, 0xD3, 0x6A, 0x8C, 0x2E, 0x4C, 0x42, 0xAF,
0xDE, 0x32, 0xC1, 0x4B, 0x5A, 0x61, 0x6D, 0xF9, 0xA3, 0xB3, 0xCA, 0x1D, 0xAB, 0x13, 0xE3, 0x14,
0xAC, 0xBB, 0xF3, 0x33, 0xA7, 0xDA, 0x30, 0xFA, 0xED, 0x40, 0xBB, 0x6A, 0x62, 0xC0, 0x30, 0x8A,
0xFD, 0x9A, 0xDB, 0xF4, 0x49, 0x7B, 0xA6, 0x3B, 0x17, 0x90, 0xD6, 0x2E, 0x79, 0x2D, 0xCF, 0x63,
0xE4, 0xB8, 0x1F, 0x5B, 0xD1, 0xDC, 0x8A, 0xD3, 0xF0, 0xBB, 0xBF, 0x73, 0xEF, 0x11, 0xE2, 0x0F,
0x29, 0xF8, 0xEC, 0xAE, 0xF3, 0x07, 0x5B, 0x11, 0x5F, 0x90, 0xB0, 0x53, 0xAE, 0x65, 0xF6, 0x5C,
0x1F, 0x44, 0x80, 0x4F, 0xC1, 0x83, 0x63, 0x9F, 0xE1, 0xAA, 0xE3, 0xF8, 0xBF, 0xB1, 0x51, 0x66,
0x19, 0x19, 0x13, 0xA0, 0xF7, 0x6D, 0xEF, 0x13, 0x97, 0x12, 0x75, 0xAC, 0xB7, 0x8C, 0x60, 0x3F,
0xC5, 0x71, 0x9B, 0xBE, 0x17, 0x26, 0xA1, 0x97, 0xB7, 0x0D, 0x6A, 0xE9, 0x28, 0x99, 0x68, 0x79,
0x1E, 0x78, 0x74, 0x56, 0x39, 0xF4, 0x5D, 0x75, 0x23, 0x7A, 0xB6, 0xEF, 0xFE, 0x22, 0x73, 0xAA,
0x0D, 0xE5, 0x01, 0x5A, 0xD0, 0x89, 0x2A, 0xE7, 0x0F, 0x95, 0x51, 0xEC, 0xD7, 0xE4, 0x2F, 0x7C,
0x4B, 0xAC, 0xEC, 0x3D, 0x88, 0x7C, 0x5A, 0xBB, 0xE4, 0xD5, 0x50, 0x41, 0x56, 0xC5, 0xBC, 0x7C,
0x63, 0x93, 0xBA, 0x15, 0xA7, 0x61, 0xC8, 0x47, 0xFA, 0x65, 0x1B, 0x07, 0x97, 0xD2, 0x2C, 0xF8,
0xEC, 0xAE, 0x35, 0x29, 0x6E, 0xDA, 0x0E, 0x6D, 0x84, 0x5E, 0xBD, 0x65, 0xF6, 0x5C, 0x27, 0xCD,
0xCC, 0x73, 0x80, 0xF6, 0xB2, 0xCA, 0x1D, 0xAB, 0xE3, 0xF8, 0xDF, 0xD5, 0x83, 0xF7, 0x15, 0xE4,
0x50, 0x6D, 0x18, 0xFD, 0xB6, 0xF7, 0x09, 0xDC, 0x51, 0x7F, 0xA0, 0xB8, 0x57, 0xB0, 0x5F, 0x73,
0x9B, 0xBE, 0x17, 0x26, 0x42, 0x42, 0xC4, 0x83, 0xAF, 0xE9, 0x92, 0xD7, 0xF2, 0x3C, 0xF0, 0xE8,
0x30, 0x1D, 0x1B, 0x94, 0xE0, 0x47, 0x9C, 0x86, 0xDF, 0xFD, 0x45, 0xE6, 0x64, 0xC5, 0x94, 0x64,
0x8C, 0xA4, 0xB3, 0xBB, 0xCE, 0x1F, 0x2A, 0xA3, 0x18, 0x58, 0xF4, 0xE2, 0x59, 0xA6, 0xD8, 0x73,
0x7D, 0x10, 0xF9, 0xB4, 0x76, 0x6A, 0x56, 0xCE, 0xD8, 0x15, 0xC7, 0xFF, 0x8D, 0x4D, 0xEA, 0x56,
0xA4, 0xDB, 0x86, 0x50, 0xD5, 0x99, 0xBD, 0x4F, 0x5C, 0x4A, 0xB3, 0xE0, 0xD3, 0x0F, 0x6C, 0x6A,
0x69, 0x71, 0x7B, 0x21, 0xF4, 0xEA, 0x2D, 0xB3, 0x08, 0xE5, 0x95, 0xEC, 0xDB, 0x03, 0x1E, 0xAB,
0xDC, 0xB1, 0x3A, 0x96, 0x50, 0xC3, 0x6E, 0x64, 0x41, 0x91, 0xA9, 0x0D, 0xA3, 0xDF, 0x36, 0x27,
0xEA, 0x5D, 0xE3, 0xA5, 0x0F, 0xCA, 0xE8, 0xD7, 0xDC, 0xA6, 0xEF, 0x26, 0x74, 0x5D, 0xC0, 0xCD,
0x78, 0x5A, 0xC9, 0x6B, 0x79, 0x1E, 0x80, 0xC9, 0xFF, 0x8C, 0x96, 0x79, 0x84, 0xBA, 0x4D, 0xC3,
0xEF, 0xFE, 0x42, 0xC7, 0x4F, 0x58, 0xE0, 0x2D, 0x59, 0xB0, 0xBB, 0xCE, 0x1F, 0x2A, 0x44, 0xC3,
0x04, 0xA4, 0xBF, 0xF1, 0x96, 0xE7, 0xFA, 0x20, 0xF2, 0x71, 0x42, 0x3A, 0x2A, 0x42, 0xD0, 0x58,
0x8D, 0xFF, 0x1B, 0x9B, 0x14, 0x56, 0x73, 0xA2, 0x39, 0x96, 0xD0, 0xEF, 0x3E, 0x71, 0x29, 0xCD,
0xC4, 0xA4, 0x98, 0x6F, 0x89, 0xE9, 0x54, 0xB5, 0xE9, 0xC2, 0x24, 0xF4, 0xEA, 0xB1, 0x5D, 0x3B,
0x64, 0x55, 0x44, 0x9E, 0x3F, 0x3A, 0xAB, 0xDC, 0xD1, 0x8E, 0x2B, 0x4A, 0xBF, 0x2C, 0x77, 0x3F,
0x73, 0xAA, 0x0D, 0xA3, 0x00, 0xE1, 0x93, 0x9B, 0xB6, 0xE1, 0x0F, 0xA3, 0xD8, 0xAF, 0xB9, 0x55,
0x30, 0xB3, 0xE6, 0x39, 0x50, 0xD0, 0xDA, 0x25, 0xAF, 0x65, 0x8A, 0x75, 0x0C, 0xEF, 0x53, 0xBD,
0x60, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xBF, 0x70, 0xEF, 0xBF, 0xB0, 0xFB, 0x37, 0xF4, 0xFD,
0x0D, 0xDD, 0xDF, 0x85, 0xEF, 0xEF, 0x89, 0xF7, 0xFB, 0xC4, 0xFB, 0x3E, 0x78, 0xF7, 0x13, 0xDF,
0x7D, 0xC5, 0xB7, 0x5F, 0xF8, 0xF6, 0x0B, 0x5F, 0x7F, 0xE1, 0xED, 0x2F, 0xDC, 0xFD, 0x85, 0xBD,
0xDF, 0xD0, 0xF7, 0xDF, 0xC1, 0xF7, 0x77, 0xF0, 0x7D, 0x0F, 0xBE, 0xEF, 0x83, 0xEF, 0xFB, 0xE0,
0xBD, 0x1F, 0xBC, 0xF7, 0x0B, 0x77, 0xBF, 0x70, 0xF7, 0x0B, 0xD7, 0xBF, 0x70, 0xFD, 0x0B, 0xD7,
0xBF, 0xB0, 0xFD, 0x1D, 0xBA, 0xDF, 0x83, 0xF7, 0x7B, 0x70, 0xDF, 0x87, 0xDE, 0xF7, 0x83, 0xFB,
0xFE, 0xE0, 0xDE, 0x2F, 0xDC, 0xFD, 0x85, 0xDB, 0xDF, 0x70, 0xFB, 0x1B, 0xAE, 0x7F, 0xC3, 0xF5,
0x6F, 0xD8, 0xFE, 0x0D, 0xDB, 0xDF, 0xA1, 0xFB, 0x3B, 0x78, 0xBF, 0x07, 0xF7, 0xF7, 0xE0, 0x7E,
0x1F, 0xDC, 0xF7, 0x83, 0x7B, 0x3F, 0xB8, 0xF7, 0x07, 0x77, 0xBF, 0x70, 0xFB, 0x0B, 0xD7, 0xBF,
0xF0, 0xFA, 0x17, 0xB6, 0xBF, 0x61, 0xF7, 0x37, 0x74, 0xBF, 0x83, 0xF7, 0x3D, 0xB8, 0xDF, 0x83,
0xFB, 0x3E, 0x78, 0xDF, 0x0F, 0xDE, 0xFD, 0xE0, 0xDD, 0x17, 0xDE, 0x7E, 0xE1, 0xF5, 0x0B, 0x0F,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x20, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xA8, 0x01, 0x00,
0x00, 0x53, 0x02, 0x00, 0x00, 0xFD, 0x02, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x88, 0x03, 0x00,
0x00, 0x89, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00,
0x00, 0x89, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x27, 0x3B, 0x00, 0x18, 0x2B, 0x42, 0x57, 0x6D, 0x81,
0x98, 0x00, 0x17, 0x2B, 0x42, 0x56, 0x6D, 0x80, 0x97, 0x00, 0x17, 0x2B, 0x43, 0x56, 0x6D, 0x80,
0x97, 0x00, 0x16, 0x2B, 0x40, 0x55, 0x69, 0x80, 0x94, 0x00, 0x13, 0x29, 0x3E, 0x52, 0x68, 0x7C,
0x89, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1C, 0x00,
0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2D, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x01, 0x80,
0x00, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00,
0x10, 0x00, 0x00, 0x02, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00,
0x20, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x40, 0x00, 0x00,
0x20, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x40, 0x00, 0x00,
0x20, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00,
0x08, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x80, 0x00, 0x00, 0x20, 0x00, 0x00,
0x01, 0x00, 0x40, 0x00, 0x00, 0x08, 0x00, 0x80, 0x00, 0x00, 0x20, 0x00, 0x00, 0x02, 0x40, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00,
0x19, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x05, 0x07, 0x08, 0x0A, 0x0B,
0x00, 0x01, 0x02, 0x04, 0x06, 0x07, 0x09, 0x0A, 0x00, 0x01, 0x03, 0x04, 0x06, 0x07, 0x09, 0x0A,
0x00, 0x01, 0x03, 0x04, 0x06, 0x14, 0x07, 0x00, 0x00, 0xAB, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x81, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00,
0x00, 0x81, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, 0x00, 0x11, 0x21,
0x31, 0x41, 0x51, 0x61, 0x71, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x10, 0x20,
0x30, 0x40, 0x50, 0x60, 0x70, 0x00, 0x01, 0x14, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x38, 0x8E, 0xE3, 0x38, 0x8E,
0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3,
0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x38,
0x8E, 0xE3, 0x38, 0x8E, 0xE3, 0x18, 0x00, 0x00, 0x02, 0x00, 0x00, 0x51, 0x14, 0x45, 0x51, 0x14,
0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45,
0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51,
0x14, 0x45, 0x51, 0x14, 0x45, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x15, 0x20, 0x2B, 0x35, 0x40, 0x4B, 0x00,
0x0B, 0x16, 0x1D, 0x1D, 0x1D, 0x1D, 0x1D, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00,
0x01, 0x08, 0x20, 0x00, 0x01, 0x08, 0x20, 0x00, 0x01, 0x08, 0x20, 0x00, 0x01, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x09, 0x72, 0x00, 0x00,
0x00, 0xAB, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x21, 0x31, 0x01, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0x8E,
0x23, 0x00, 0x20, 0x00, 0x00, 0x00, 0x51, 0x14, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x8A, 0x03, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x30, 0x00,
0x00, 0x00, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A,
0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x00, 0x02, 0x04, 0x06, 0x08,
};
}
}

View File

@@ -0,0 +1,16 @@
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
struct MatchCheckState
{
public uint CheckMask;
public readonly uint RegionMask;
public readonly ProfanityFilterOption Option;
public MatchCheckState(uint checkMask, uint regionMask, ProfanityFilterOption option)
{
CheckMask = checkMask;
RegionMask = regionMask;
Option = option;
}
}
}

View File

@@ -0,0 +1,24 @@
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
struct MatchDelimitedState
{
public bool Matched;
public readonly bool PrevCharIsWordSeparator;
public readonly bool NextCharIsWordSeparator;
public readonly Sbv NoSeparatorMap;
public readonly AhoCorasick DelimitedWordsTrie;
public MatchDelimitedState(
bool prevCharIsWordSeparator,
bool nextCharIsWordSeparator,
Sbv noSeparatorMap,
AhoCorasick delimitedWordsTrie)
{
Matched = false;
PrevCharIsWordSeparator = prevCharIsWordSeparator;
NextCharIsWordSeparator = nextCharIsWordSeparator;
NoSeparatorMap = noSeparatorMap;
DelimitedWordsTrie = delimitedWordsTrie;
}
}
}

View File

@@ -0,0 +1,113 @@
using System;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
readonly struct MatchRange
{
public readonly int StartOffset;
public readonly int EndOffset;
public MatchRange(int startOffset, int endOffset)
{
StartOffset = startOffset;
EndOffset = endOffset;
}
}
struct MatchRangeList
{
private int _capacity;
private int _count;
private MatchRange[] _ranges;
public readonly int Count => _count;
public readonly MatchRange this[int index] => _ranges[index];
public MatchRangeList()
{
_capacity = 0;
_count = 0;
_ranges = Array.Empty<MatchRange>();
}
public void Add(int startOffset, int endOffset)
{
if (_count == _capacity)
{
int newCapacity = _count * 2;
if (newCapacity == 0)
{
newCapacity = 1;
}
Array.Resize(ref _ranges, newCapacity);
_capacity = newCapacity;
}
_ranges[_count++] = new(startOffset, endOffset);
}
public readonly MatchRangeList Deduplicate()
{
MatchRangeList output = new();
if (_count != 0)
{
int prevStartOffset = _ranges[0].StartOffset;
int prevEndOffset = _ranges[0].EndOffset;
for (int index = 1; index < _count; index++)
{
int currStartOffset = _ranges[index].StartOffset;
int currEndOffset = _ranges[index].EndOffset;
if (prevStartOffset == currStartOffset)
{
if (prevEndOffset <= currEndOffset)
{
prevEndOffset = currEndOffset;
}
}
else if (prevEndOffset <= currStartOffset)
{
output.Add(prevStartOffset, prevEndOffset);
prevStartOffset = currStartOffset;
prevEndOffset = currEndOffset;
}
}
output.Add(prevStartOffset, prevEndOffset);
}
return output;
}
public readonly int Find(int startOffset, int endOffset)
{
int baseIndex = 0;
int range = _count;
while (range != 0)
{
MatchRange currRange = _ranges[baseIndex + (range / 2)];
if (currRange.StartOffset < startOffset || (currRange.StartOffset == startOffset && currRange.EndOffset < endOffset))
{
int nextHalf = (range / 2) + 1;
baseIndex += nextHalf;
range -= nextHalf;
}
else
{
range /= 2;
}
}
return baseIndex;
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
struct MatchRangeListState
{
public MatchRangeList MatchRanges;
public MatchRangeListState()
{
MatchRanges = new();
}
public static bool AddMatch(ReadOnlySpan<byte> text, int startOffset, int endOffset, int nodeId, ref MatchRangeListState state)
{
state.MatchRanges.Add(startOffset, endOffset);
return true;
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
struct MatchSimilarFormState
{
public MatchRangeList MatchRanges;
public SimilarFormTable SimilarFormTable;
public Utf8Text CanonicalText;
public int ReplaceEndOffset;
public MatchSimilarFormState(MatchRangeList matchRanges, SimilarFormTable similarFormTable)
{
MatchRanges = matchRanges;
SimilarFormTable = similarFormTable;
CanonicalText = new();
ReplaceEndOffset = 0;
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
readonly ref struct MatchState
{
public readonly Span<byte> OriginalText;
public readonly Span<byte> ConvertedText;
public readonly ReadOnlySpan<sbyte> DeltaTable;
public readonly ref int MaskedCount;
public readonly MaskMode MaskMode;
public readonly Sbv NoSeparatorMap;
public readonly AhoCorasick DelimitedWordsTrie;
public MatchState(
Span<byte> originalText,
Span<byte> convertedText,
ReadOnlySpan<sbyte> deltaTable,
ref int maskedCount,
MaskMode maskMode,
Sbv noSeparatorMap = null,
AhoCorasick delimitedWordsTrie = null)
{
OriginalText = originalText;
ConvertedText = convertedText;
DeltaTable = deltaTable;
MaskedCount = ref maskedCount;
MaskMode = maskMode;
NoSeparatorMap = noSeparatorMap;
DelimitedWordsTrie = delimitedWordsTrie;
}
public readonly (int, int) GetOriginalRange(int convertedStartOffest, int convertedEndOffset)
{
int originalStartOffset = 0;
int originalEndOffset = 0;
for (int index = 0; index < convertedEndOffset; index++)
{
int byteLength = Math.Abs(DeltaTable[index]);
originalStartOffset += index < convertedStartOffest ? byteLength : 0;
originalEndOffset += byteLength;
}
return (originalStartOffset, originalEndOffset);
}
}
}

View File

@@ -0,0 +1,886 @@
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Fs;
using System;
using System.Buffers.Binary;
using System.Numerics;
using System.Text;
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
{
class ProfanityFilter : ProfanityFilterBase, IDisposable
{
private const int MaxBufferLength = 0x800;
private const int MaxUtf8CharacterLength = 4;
private const int MaxUtf8Characters = MaxBufferLength / MaxUtf8CharacterLength;
private const int RegionsCount = 16;
private const int MountCacheSize = 0x2000;
private readonly ContentsReader _contentsReader;
public ProfanityFilter(IFsClient fsClient)
{
_contentsReader = new(fsClient);
}
public Result Initialize()
{
return _contentsReader.Initialize(MountCacheSize);
}
public override Result Reload()
{
return _contentsReader.Reload();
}
public override Result GetContentVersion(out uint version)
{
version = 0;
Result result = _contentsReader.GetVersionDataSize(out long size);
if (result.IsFailure && size != 4)
{
return Result.Success;
}
Span<byte> data = stackalloc byte[4];
result = _contentsReader.GetVersionData(data);
if (result.IsFailure)
{
return Result.Success;
}
version = BinaryPrimitives.ReadUInt32BigEndian(data);
return Result.Success;
}
public override Result CheckProfanityWords(out uint checkMask, ReadOnlySpan<byte> word, uint regionMask, ProfanityFilterOption option)
{
checkMask = 0;
int length = word.IndexOf((byte)0);
if (length >= 0)
{
word = word[..length];
}
UTF8Encoding encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
string decodedWord;
try
{
decodedWord = encoding.GetString(word);
}
catch (ArgumentException)
{
return NgcResult.InvalidUtf8Encoding;
}
return CheckProfanityWordsMultiRegionImpl(ref checkMask, decodedWord, regionMask, option);
}
private Result CheckProfanityWordsMultiRegionImpl(ref uint checkMask, string word, uint regionMask, ProfanityFilterOption option)
{
// Check using common dictionary.
Result result = CheckProfanityWordsImpl(ref checkMask, word, 0, option);
if (result.IsFailure)
{
return result;
}
if (checkMask != 0)
{
checkMask = (ushort)(regionMask | option.SystemRegionMask);
}
// Check using region specific dictionaries if needed.
for (int regionIndex = 0; regionIndex < RegionsCount; regionIndex++)
{
if (((regionMask | option.SystemRegionMask) & (1 << regionIndex)) != 0)
{
result = CheckProfanityWordsImpl(ref checkMask, word, 1u << regionIndex, option);
if (result.IsFailure)
{
return result;
}
}
}
return Result.Success;
}
private Result CheckProfanityWordsImpl(ref uint checkMask, string word, uint regionMask, ProfanityFilterOption option)
{
ConvertUserInputForWord(out string convertedWord, word);
if (IsIncludesAtSign(convertedWord))
{
checkMask |= regionMask != 0 ? regionMask : option.SystemRegionMask;
}
byte[] utf8Text = Encoding.UTF8.GetBytes(convertedWord);
byte[] convertedText = new byte[utf8Text.Length + 5];
utf8Text.CopyTo(convertedText.AsSpan().Slice(2, utf8Text.Length));
convertedText[0] = (byte)'\\';
convertedText[1] = (byte)'b';
convertedText[2 + utf8Text.Length] = (byte)'\\';
convertedText[3 + utf8Text.Length] = (byte)'b';
convertedText[4 + utf8Text.Length] = 0;
int regionIndex = (ushort)regionMask != 0 ? BitOperations.TrailingZeroCount(regionMask) : -1;
Result result = _contentsReader.ReadDictionaries(out AhoCorasick partialWordsTrie, out _, out AhoCorasick delimitedWordsTrie, regionIndex);
if (result.IsFailure)
{
return result;
}
if ((checkMask & regionMask) == 0)
{
MatchCheckState state = new(checkMask, regionMask, option);
partialWordsTrie.Match(convertedText, MatchCheck, ref state);
delimitedWordsTrie.Match(convertedText, MatchCheck, ref state);
checkMask = state.CheckMask;
}
return Result.Success;
}
public override Result MaskProfanityWordsInText(out int maskedWordsCount, Span<byte> text, uint regionMask, ProfanityFilterOption option)
{
maskedWordsCount = 0;
Span<byte> output = text;
Span<byte> convertedText = new byte[MaxBufferLength];
Span<sbyte> deltaTable = new sbyte[MaxBufferLength];
int nullTerminatorIndex = GetUtf8Length(out _, text, MaxUtf8Characters);
// Ensure that the text has a null terminator if we can.
// If the text is too long, it will be truncated.
byte replacedCharacter = 0;
if (nullTerminatorIndex > 0 && nullTerminatorIndex < text.Length)
{
replacedCharacter = text[nullTerminatorIndex];
text[nullTerminatorIndex] = 0;
}
// Truncate the text if needed.
int length = text.IndexOf((byte)0);
if (length >= 0)
{
text = text[..length];
}
// If requested, mask e-mail addresses.
if (option.SkipAtSignCheck == SkipMode.DoNotSkip)
{
maskedWordsCount += FilterAtSign(text, option.MaskMode);
text = MaskText(text);
}
// Convert the text to lower case, required for string matching.
ConvertUserInputForText(convertedText, deltaTable, text);
// Mask words for common and requested regions.
Result result = MaskProfanityWordsInTextMultiRegion(ref maskedWordsCount, ref text, ref convertedText, deltaTable, regionMask, option);
if (result.IsFailure)
{
return result;
}
// If requested, also try to match and mask the canonicalized string.
if (option.Flags != ProfanityFilterFlags.None)
{
result = MaskProfanityWordsInTextCanonicalizedMultiRegion(ref maskedWordsCount, text, regionMask, option);
if (result.IsFailure)
{
return result;
}
}
// If we received more text than we can process, copy unprocessed portion to the end of the new text.
if (replacedCharacter != 0)
{
length = text.IndexOf((byte)0);
if (length < 0)
{
length = text.Length;
}
output[length++] = replacedCharacter;
int unprocessedLength = output.Length - nullTerminatorIndex - 1;
output.Slice(nullTerminatorIndex + 1, unprocessedLength).CopyTo(output.Slice(length, unprocessedLength));
}
return Result.Success;
}
private Result MaskProfanityWordsInTextMultiRegion(
ref int maskedWordsCount,
ref Span<byte> originalText,
ref Span<byte> convertedText,
Span<sbyte> deltaTable,
uint regionMask,
ProfanityFilterOption option)
{
// Filter using common dictionary.
Result result = MaskProfanityWordsInTextImpl(ref maskedWordsCount, ref originalText, ref convertedText, deltaTable, -1, option);
if (result.IsFailure)
{
return result;
}
// Filter using region specific dictionaries if needed.
for (int regionIndex = 0; regionIndex < RegionsCount; regionIndex++)
{
if (((regionMask | option.SystemRegionMask) & (1 << regionIndex)) != 0)
{
result = MaskProfanityWordsInTextImpl(ref maskedWordsCount, ref originalText, ref convertedText, deltaTable, regionIndex, option);
if (result.IsFailure)
{
return result;
}
}
}
return Result.Success;
}
private Result MaskProfanityWordsInTextImpl(
ref int maskedWordsCount,
ref Span<byte> originalText,
ref Span<byte> convertedText,
Span<sbyte> deltaTable,
int regionIndex,
ProfanityFilterOption option)
{
Result result = _contentsReader.ReadDictionaries(
out AhoCorasick partialWordsTrie,
out AhoCorasick completeWordsTrie,
out AhoCorasick delimitedWordsTrie,
regionIndex);
if (result.IsFailure)
{
return result;
}
// Match single words.
MatchState state = new(originalText, convertedText, deltaTable, ref maskedWordsCount, option.MaskMode);
partialWordsTrie.Match(convertedText, MatchSingleWord, ref state);
MaskText(ref originalText, ref convertedText, deltaTable);
// Match single words and phrases.
// We remove word separators on the string used for the match.
Span<byte> noSeparatorText = new byte[originalText.Length];
Sbv noSeparatorMap = new(convertedText.Length);
noSeparatorText = RemoveWordSeparators(noSeparatorText, convertedText, noSeparatorMap);
state = new(
originalText,
convertedText,
deltaTable,
ref maskedWordsCount,
option.MaskMode,
noSeparatorMap,
delimitedWordsTrie);
partialWordsTrie.Match(noSeparatorText, MatchMultiWord, ref state);
MaskText(ref originalText, ref convertedText, deltaTable);
// Match whole words, which must be surrounded by word separators.
noSeparatorText = new byte[originalText.Length];
noSeparatorMap = new(convertedText.Length);
noSeparatorText = RemoveWordSeparators(noSeparatorText, convertedText, noSeparatorMap);
state = new(
originalText,
convertedText,
deltaTable,
ref maskedWordsCount,
option.MaskMode,
noSeparatorMap,
delimitedWordsTrie);
completeWordsTrie.Match(noSeparatorText, MatchDelimitedWord, ref state);
MaskText(ref originalText, ref convertedText, deltaTable);
return Result.Success;
}
private static void MaskText(ref Span<byte> originalText, ref Span<byte> convertedText, Span<sbyte> deltaTable)
{
originalText = MaskText(originalText);
UpdateDeltaTable(deltaTable, convertedText);
convertedText = MaskText(convertedText);
}
private Result MaskProfanityWordsInTextCanonicalizedMultiRegion(ref int maskedWordsCount, Span<byte> text, uint regionMask, ProfanityFilterOption option)
{
// Filter using common dictionary.
Result result = MaskProfanityWordsInTextCanonicalized(ref maskedWordsCount, text, 0, option);
if (result.IsFailure)
{
return result;
}
// Filter using region specific dictionaries if needed.
for (int index = 0; index < RegionsCount; index++)
{
if ((((regionMask | option.SystemRegionMask) >> index) & 1) != 0)
{
result = MaskProfanityWordsInTextCanonicalized(ref maskedWordsCount, text, 1u << index, option);
if (result.IsFailure)
{
return result;
}
}
}
return Result.Success;
}
private Result MaskProfanityWordsInTextCanonicalized(ref int maskedWordsCount, Span<byte> text, uint regionMask, ProfanityFilterOption option)
{
Utf8Text maskedText = new();
Utf8ParseResult parseResult = Utf8Text.Create(out Utf8Text inputText, text);
if (parseResult != Utf8ParseResult.Success)
{
return NgcResult.InvalidUtf8Encoding;
}
ReadOnlySpan<byte> prevCharacter = ReadOnlySpan<byte>.Empty;
int charStartIndex = 0;
for (int charEndIndex = 1; charStartIndex < inputText.CharacterCount;)
{
ReadOnlySpan<byte> nextCharacter = charEndIndex < inputText.CharacterCount
? inputText.AsSubstring(charEndIndex, charEndIndex + 1)
: ReadOnlySpan<byte>.Empty;
Result result = CheckProfanityWordsInTextCanonicalized(
out bool matched,
inputText.AsSubstring(charStartIndex, charEndIndex),
prevCharacter,
nextCharacter,
regionMask,
option);
if (result.IsFailure && result != NgcResult.InvalidSize)
{
return result;
}
if (matched)
{
// We had a match, we know where it ends, now we need to find where it starts.
int previousCharStartIndex = charStartIndex;
for (; charStartIndex < charEndIndex; charStartIndex++)
{
result = CheckProfanityWordsInTextCanonicalized(
out matched,
inputText.AsSubstring(charStartIndex, charEndIndex),
prevCharacter,
nextCharacter,
regionMask,
option);
if (result.IsFailure && result != NgcResult.InvalidSize)
{
return result;
}
// When we get past the start of the matched substring, the match will fail,
// so that's when we know we found the start.
if (!matched)
{
break;
}
}
// Append substring before the match start.
maskedText = maskedText.Append(inputText.AsSubstring(previousCharStartIndex, charStartIndex - 1));
// Mask matched substring with asterisks.
if (option.MaskMode == MaskMode.ReplaceByOneCharacter)
{
maskedText = maskedText.Append("*"u8);
prevCharacter = "*"u8;
}
else if (option.MaskMode == MaskMode.Overwrite && charStartIndex <= charEndIndex)
{
int maskLength = charEndIndex - charStartIndex + 1;
while (maskLength-- > 0)
{
maskedText = maskedText.Append("*"u8);
}
prevCharacter = "*"u8;
}
charStartIndex = charEndIndex;
maskedWordsCount++;
}
if (charEndIndex < inputText.CharacterCount)
{
charEndIndex++;
}
else if (charStartIndex < inputText.CharacterCount)
{
prevCharacter = inputText.AsSubstring(charStartIndex, charStartIndex + 1);
maskedText = maskedText.Append(prevCharacter);
charStartIndex++;
}
}
// Replace text with the masked text.
maskedText.CopyTo(text);
return Result.Success;
}
private Result CheckProfanityWordsInTextCanonicalized(
out bool matched,
ReadOnlySpan<byte> text,
ReadOnlySpan<byte> prevCharacter,
ReadOnlySpan<byte> nextCharacter,
uint regionMask,
ProfanityFilterOption option)
{
matched = false;
Span<byte> convertedText = new byte[MaxBufferLength + 1];
text.CopyTo(convertedText[..text.Length]);
Result result;
if (text.Length > 0)
{
// If requested, normalize.
// This will convert different encodings for the same character in their canonical encodings.
if (option.Flags.HasFlag(ProfanityFilterFlags.MatchNormalizedFormKC))
{
Utf8ParseResult parseResult = Utf8Util.NormalizeFormKC(convertedText, convertedText);
if (parseResult != Utf8ParseResult.Success)
{
return NgcResult.InvalidUtf8Encoding;
}
}
// Convert to lower case.
ConvertUserInputForText(convertedText, Span<sbyte>.Empty, convertedText);
// If requested, also try to replace similar characters with their canonical form.
// For example, vv is similar to w, and 1 or | is similar to i.
if (option.Flags.HasFlag(ProfanityFilterFlags.MatchSimilarForm))
{
result = ConvertInputTextFromSimilarForm(convertedText, convertedText);
if (result.IsFailure)
{
return result;
}
}
int length = convertedText.IndexOf((byte)0);
if (length >= 0)
{
convertedText = convertedText[..length];
}
}
int regionIndex = (ushort)regionMask != 0 ? BitOperations.TrailingZeroCount(regionMask) : -1;
result = _contentsReader.ReadDictionaries(
out AhoCorasick partialWordsTrie,
out AhoCorasick completeWordsTrie,
out AhoCorasick delimitedWordsTrie,
regionIndex);
if (result.IsFailure)
{
return result;
}
result = ContentsReader.ReadNotSeparatorDictionary(out AhoCorasick notSeparatorTrie);
if (result.IsFailure)
{
return result;
}
// Match single words.
bool trieMatched = false;
partialWordsTrie.Match(convertedText, MatchSimple, ref trieMatched);
if (trieMatched)
{
matched = true;
return Result.Success;
}
// Match single words and phrases.
// We remove word separators on the string used for the match.
Span<byte> noSeparatorText = new byte[text.Length];
Sbv noSeparatorMap = new(convertedText.Length);
noSeparatorText = RemoveWordSeparators(noSeparatorText, convertedText, noSeparatorMap, notSeparatorTrie);
trieMatched = false;
partialWordsTrie.Match(noSeparatorText, MatchSimple, ref trieMatched);
if (trieMatched)
{
matched = true;
return Result.Success;
}
// Match whole words, which must be surrounded by word separators.
bool prevCharIsWordSeparator = prevCharacter.Length == 0 || IsWordSeparator(prevCharacter, notSeparatorTrie);
bool nextCharIsWordSeparator = nextCharacter.Length == 0 || IsWordSeparator(nextCharacter, notSeparatorTrie);
MatchDelimitedState state = new(prevCharIsWordSeparator, nextCharIsWordSeparator, noSeparatorMap, delimitedWordsTrie);
completeWordsTrie.Match(noSeparatorText, MatchDelimitedWordSimple, ref state);
if (state.Matched)
{
matched = true;
}
return Result.Success;
}
private Result ConvertInputTextFromSimilarForm(Span<byte> convertedText, ReadOnlySpan<byte> text)
{
int length = text.IndexOf((byte)0);
if (length >= 0)
{
text = text[..length];
}
Result result = _contentsReader.ReadSimilarFormDictionary(out AhoCorasick similarFormTrie);
if (result.IsFailure)
{
return result;
}
result = _contentsReader.ReadSimilarFormTable(out SimilarFormTable similarFormTable);
if (result.IsFailure)
{
return result;
}
// Find all characters that have a similar form.
MatchRangeListState listState = new();
similarFormTrie.Match(text, MatchRangeListState.AddMatch, ref listState);
// Filter found match ranges.
// Because some similar form strings are a subset of others, we need to remove overlapping matches.
// For example, | can be replaced with i, but |-| can be replaced with h.
// We prefer the latter match (|-|) because it is more specific.
MatchRangeList deduplicatedMatches = listState.MatchRanges.Deduplicate();
MatchSimilarFormState state = new(deduplicatedMatches, similarFormTable);
similarFormTrie.Match(text, MatchAndReplace, ref state);
// Append remaining characters.
state.CanonicalText = state.CanonicalText.Append(text[state.ReplaceEndOffset..]);
// Set canonical text to output.
ReadOnlySpan<byte> canonicalText = state.CanonicalText.AsSpan();
canonicalText.CopyTo(convertedText[..canonicalText.Length]);
convertedText[canonicalText.Length] = 0;
return Result.Success;
}
private static bool MatchCheck(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchCheckState state)
{
state.CheckMask |= state.RegionMask != 0 ? state.RegionMask : state.Option.SystemRegionMask;
return true;
}
private static bool MatchSingleWord(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state)
{
MatchCommon(ref state, matchStartOffset, matchEndOffset);
return true;
}
private static bool MatchMultiWord(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state)
{
int convertedStartOffset = state.NoSeparatorMap.Set.Select0(matchStartOffset);
int convertedEndOffset = state.NoSeparatorMap.Set.Select0(matchEndOffset);
if (convertedEndOffset < 0)
{
convertedEndOffset = state.NoSeparatorMap.Set.BitVector.BitLength;
}
int endOffsetBeforeSeparator = TrimEnd(state.ConvertedText, convertedEndOffset);
MatchCommon(ref state, convertedStartOffset, endOffsetBeforeSeparator);
return true;
}
private static bool MatchDelimitedWord(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchState state)
{
int convertedStartOffset = state.NoSeparatorMap.Set.Select0(matchStartOffset);
int convertedEndOffset = state.NoSeparatorMap.Set.Select0(matchEndOffset);
if (convertedEndOffset < 0)
{
convertedEndOffset = state.NoSeparatorMap.Set.BitVector.BitLength;
}
int endOffsetBeforeSeparator = TrimEnd(state.ConvertedText, convertedEndOffset);
Span<byte> delimitedText = new byte[64];
// If the word is prefixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimitar.
// The start of the string is also considered a "word separator".
bool startIsPrefixedByWordSeparator =
convertedStartOffset == 0 ||
IsPrefixedByWordSeparator(state.ConvertedText, convertedStartOffset);
int delimitedTextOffset = 0;
if (startIsPrefixedByWordSeparator)
{
delimitedText[delimitedTextOffset++] = (byte)'\\';
delimitedText[delimitedTextOffset++] = (byte)'b';
}
else
{
delimitedText[delimitedTextOffset++] = (byte)'a';
}
// Copy the word to our temporary buffer used for the next match.
int matchLength = matchEndOffset - matchStartOffset;
text.Slice(matchStartOffset, matchLength).CopyTo(delimitedText.Slice(delimitedTextOffset, matchLength));
delimitedTextOffset += matchLength;
// If the word is suffixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimiter.
// The end of the string is also considered a "word separator".
bool endIsSuffixedByWordSeparator =
endOffsetBeforeSeparator == state.NoSeparatorMap.Set.BitVector.BitLength ||
state.ConvertedText[endOffsetBeforeSeparator] == 0 ||
IsWordSeparator(state.ConvertedText, endOffsetBeforeSeparator);
if (endIsSuffixedByWordSeparator)
{
delimitedText[delimitedTextOffset++] = (byte)'\\';
delimitedText[delimitedTextOffset++] = (byte)'b';
}
else
{
delimitedText[delimitedTextOffset++] = (byte)'a';
}
// Create our temporary match state for the next match.
bool matched = false;
// Insert the null terminator.
delimitedText[delimitedTextOffset] = 0;
// Check if the delimited word is on the dictionary.
state.DelimitedWordsTrie.Match(delimitedText, MatchSimple, ref matched);
// If we have a match, mask the word.
if (matched)
{
MatchCommon(ref state, convertedStartOffset, endOffsetBeforeSeparator);
}
return true;
}
private static void MatchCommon(ref MatchState state, int matchStartOffset, int matchEndOffset)
{
// If length is zero or negative, there was no match.
if (matchStartOffset >= matchEndOffset)
{
return;
}
Span<byte> convertedText = state.ConvertedText;
Span<byte> originalText = state.OriginalText;
int matchLength = matchEndOffset - matchStartOffset;
int characterCount = Encoding.UTF8.GetCharCount(state.ConvertedText.Slice(matchStartOffset, matchLength));
// Exit early if there are no character, or if we matched past the end of the string.
if (characterCount == 0 ||
(matchStartOffset > 0 && convertedText[matchStartOffset - 1] == 0) ||
(matchStartOffset > 1 && convertedText[matchStartOffset - 2] == 0))
{
return;
}
state.MaskedCount++;
(int originalStartOffset, int originalEndOffset) = state.GetOriginalRange(matchStartOffset, matchEndOffset);
PreMaskCharacterRange(convertedText, matchStartOffset, matchEndOffset, state.MaskMode, characterCount);
PreMaskCharacterRange(originalText, originalStartOffset, originalEndOffset, state.MaskMode, characterCount);
}
private static bool MatchDelimitedWordSimple(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchDelimitedState state)
{
int convertedStartOffset = state.NoSeparatorMap.Set.Select0(matchStartOffset);
Span<byte> delimitedText = new byte[64];
// If the word is prefixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimitar.
// The start of the string is also considered a "word separator".
bool startIsPrefixedByWordSeparator =
(convertedStartOffset == 0 && state.PrevCharIsWordSeparator) ||
state.NoSeparatorMap.Set.Has(convertedStartOffset - 1);
int delimitedTextOffset = 0;
if (startIsPrefixedByWordSeparator)
{
delimitedText[delimitedTextOffset++] = (byte)'\\';
delimitedText[delimitedTextOffset++] = (byte)'b';
}
else
{
delimitedText[delimitedTextOffset++] = (byte)'a';
}
// Copy the word to our temporary buffer used for the next match.
int matchLength = matchEndOffset - matchStartOffset;
text.Slice(matchStartOffset, matchLength).CopyTo(delimitedText.Slice(delimitedTextOffset, matchLength));
delimitedTextOffset += matchLength;
// If the word is suffixed by a word separator, insert "\b" delimiter, otherwise insert "a" delimiter.
// The end of the string is also considered a "word separator".
int convertedEndOffset = state.NoSeparatorMap.Set.Select0(matchEndOffset);
bool endIsSuffixedByWordSeparator =
(convertedEndOffset < 0 && state.NextCharIsWordSeparator) ||
state.NoSeparatorMap.Set.Has(convertedEndOffset - 1);
if (endIsSuffixedByWordSeparator)
{
delimitedText[delimitedTextOffset++] = (byte)'\\';
delimitedText[delimitedTextOffset++] = (byte)'b';
}
else
{
delimitedText[delimitedTextOffset++] = (byte)'a';
}
// Create our temporary match state for the next match.
bool matched = false;
// Insert the null terminator.
delimitedText[delimitedTextOffset] = 0;
// Check if the delimited word is on the dictionary.
state.DelimitedWordsTrie.Match(delimitedText, MatchSimple, ref matched);
// If we have a match, mask the word.
if (matched)
{
state.Matched = true;
}
return !matched;
}
private static bool MatchAndReplace(ReadOnlySpan<byte> text, int matchStartOffset, int matchEndOffset, int nodeId, ref MatchSimilarFormState state)
{
if (matchStartOffset < state.ReplaceEndOffset || state.MatchRanges.Count == 0)
{
return true;
}
// Check if the match range exists on our list of ranges.
int rangeIndex = state.MatchRanges.Find(matchStartOffset, matchEndOffset);
if ((uint)rangeIndex >= (uint)state.MatchRanges.Count)
{
return true;
}
MatchRange range = state.MatchRanges[rangeIndex];
// We only replace if the match has the same size or is larger than an existing match on the list.
if (range.StartOffset <= matchStartOffset &&
(range.StartOffset != matchStartOffset || range.EndOffset <= matchEndOffset))
{
// Copy all characters since the last match to the output.
int endOffset = state.ReplaceEndOffset;
if (endOffset < matchStartOffset)
{
state.CanonicalText = state.CanonicalText.Append(text[endOffset..matchStartOffset]);
}
// Get canonical character from the similar one, and append it.
// For example, |-| is replaced with h, vv is replaced with w, etc.
ReadOnlySpan<byte> matchText = text[matchStartOffset..matchEndOffset];
state.CanonicalText = state.CanonicalText.AppendNullTerminated(state.SimilarFormTable.FindCanonicalString(matchText));
state.ReplaceEndOffset = matchEndOffset;
}
return true;
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_contentsReader.Dispose();
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

Some files were not shown because too many files have changed in this diff Show More