Merge branch 'master' into bug-6040-invert-x-and-invert-y-and-rotate-not-working-properly

This commit is contained in:
TheToid 2024-06-16 22:18:43 +10:00 committed by GitHub
commit dc3614248c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
270 changed files with 13217 additions and 3704 deletions

View file

@ -1,6 +1,7 @@
name: Feature Request
description: Suggest a new feature for Ryujinx.
title: "[Feature Request]"
labels: enhancement
body:
- type: textarea
id: overview

View file

@ -20,7 +20,7 @@ jobs:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
- { name: osx-x64, os: macOS-latest, zip_os_name: osx_x64 }
- { name: osx-x64, os: macos-13, zip_os_name: osx_x64 }
fail-fast: false
steps:
@ -41,12 +41,12 @@ jobs:
- name: Change config filename
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Change config filename for macOS
run: sed -r -i '' 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: github.event_name == 'pull_request' && matrix.platform.os == 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os == 'macos-13'
- name: Build
run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER
@ -61,15 +61,15 @@ jobs:
- name: Publish Ryujinx
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained true
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Publish Ryujinx.Headless.SDL2
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Publish Ryujinx.Gtk3
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_gtk -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Gtk3 --self-contained true
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Set executable bit
run: |
@ -83,21 +83,21 @@ jobs:
with:
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Upload Ryujinx.Headless.SDL2 artifact
uses: actions/upload-artifact@v4
with:
name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish_sdl2_headless
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Upload Ryujinx.Gtk3 artifact
uses: actions/upload-artifact@v4
with:
name: gtk-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish_gtk
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
build_macos:
name: macOS Universal (${{ matrix.configuration }})

View file

@ -8,10 +8,10 @@
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.10" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.16" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.16" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.18" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.18" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Concentus" Version="1.1.7" />
<PackageVersion Include="Concentus" Version="2.2.0" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="8.4.1" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
@ -20,9 +20,9 @@
<PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.5.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
<PackageVersion Include="NetCoreServer" Version="8.0.7" />
<PackageVersion Include="NUnit" Version="3.13.3" />
@ -42,7 +42,7 @@
<PackageVersion Include="Silk.NET.Vulkan" Version="2.16.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.16.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.16.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.7" />
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.8" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />

View file

@ -36,8 +36,8 @@
## Compatibility
As of October 2023, Ryujinx has been tested on approximately 4,200 titles;
over 4,150 boot past menus and into gameplay, with roughly 3,500 of those being considered playable.
As of May 2024, Ryujinx has been tested on approximately 4,300 titles;
over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable.
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues).

View file

@ -4,6 +4,8 @@
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="I" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="NAMESPACE" /&gt;&lt;Kind Name="CLASS" /&gt;&lt;Kind Name="STRUCT" /&gt;&lt;Kind Name="ENUM" /&gt;&lt;Kind Name="DELEGATE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="I" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ASET/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Astc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Luma/@EntryIndexedValue">True</s:Boolean>

View file

@ -237,7 +237,7 @@ namespace ARMeilleure.CodeGen.Arm64
long originalPosition = _stream.Position;
_stream.Seek(0, SeekOrigin.Begin);
_stream.Read(code, 0, code.Length);
_stream.ReadExactly(code, 0, code.Length);
_stream.Seek(originalPosition, SeekOrigin.Begin);
RelocInfo relocInfo;

View file

@ -1444,7 +1444,7 @@ namespace ARMeilleure.CodeGen.X86
Span<byte> buffer = new byte[jump.JumpPosition - _stream.Position];
_stream.Read(buffer);
_stream.ReadExactly(buffer);
_stream.Seek(ReservedBytesForJump, SeekOrigin.Current);
codeStream.Write(buffer);

View file

@ -857,8 +857,14 @@ namespace ARMeilleure.Translation.PTC
Stopwatch sw = Stopwatch.StartNew();
threads.ForEach((thread) => thread.Start());
threads.ForEach((thread) => thread.Join());
foreach (var thread in threads)
{
thread.Start();
}
foreach (var thread in threads)
{
thread.Join();
}
threads.Clear();

View file

@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Threading;
@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
return;
}
byte[] samples = new byte[frameCount * _bytesPerFrame];
using SpanOwner<byte> samplesOwner = SpanOwner<byte>.Rent(frameCount * _bytesPerFrame);
Span<byte> samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);

View file

@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Backends.SoundIo.Native;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
@ -37,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
_outputStream.WriteCallback += Update;
_outputStream.Volume = requestedVolume;
// TODO: Setup other callbacks (errors, ect).
// TODO: Setup other callbacks (errors, etc.)
_outputStream.Open();
}
@ -120,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
int channelCount = areas.Length;
byte[] samples = new byte[frameCount * bytesPerFrame];
using SpanOwner<byte> samplesOwner = SpanOwner<byte>.Rent(frameCount * bytesPerFrame);
Span<byte> samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);

View file

@ -14,7 +14,7 @@ namespace Ryujinx.Audio.Backends.Common
private readonly object _lock = new();
private IMemoryOwner<byte> _bufferOwner;
private MemoryOwner<byte> _bufferOwner;
private Memory<byte> _buffer;
private int _size;
private int _headOffset;
@ -24,7 +24,7 @@ namespace Ryujinx.Audio.Backends.Common
public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
{
_bufferOwner = ByteMemoryPool.RentCleared(initialCapacity);
_bufferOwner = MemoryOwner<byte>.RentCleared(initialCapacity);
_buffer = _bufferOwner.Memory;
}
@ -62,7 +62,7 @@ namespace Ryujinx.Audio.Backends.Common
private void SetCapacityLocked(int capacity)
{
IMemoryOwner<byte> newBufferOwner = ByteMemoryPool.RentCleared(capacity);
MemoryOwner<byte> newBufferOwner = MemoryOwner<byte>.RentCleared(capacity);
Memory<byte> newBuffer = newBufferOwner.Memory;
if (_size > 0)

View file

@ -15,7 +15,6 @@ namespace Ryujinx.Audio.Renderer.Common
{
public const int Align = 0x10;
public const int BiquadStateOffset = 0x0;
public const int BiquadStateSize = 0x10;
/// <summary>
/// The state of the biquad filters of this voice.

View file

@ -16,10 +16,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// <param name="parameter">The biquad filter parameter</param>
/// <param name="state">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessBiquadFilter(ref BiquadFilterParameter parameter, ref BiquadFilterState state, Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, uint sampleCount)
public static void ProcessBiquadFilter(
ref BiquadFilterParameter parameter,
ref BiquadFilterState state,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount)
{
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
@ -41,23 +46,24 @@ namespace Ryujinx.Audio.Renderer.Dsp
}
/// <summary>
/// Apply multiple biquad filter.
/// Apply a single biquad filter and mix the result into the output buffer.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param>
/// <param name="parameter">The biquad filter parameter</param>
/// <param name="state">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Mix volume</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessBiquadFilter(ReadOnlySpan<BiquadFilterParameter> parameters, Span<BiquadFilterState> states, Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, uint sampleCount)
public static void ProcessBiquadFilterAndMix(
ref BiquadFilterParameter parameter,
ref BiquadFilterState state,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume)
{
for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++)
{
BiquadFilterParameter parameter = parameters[stageIndex];
ref BiquadFilterState state = ref states[stageIndex];
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
@ -75,9 +81,227 @@ namespace Ryujinx.Audio.Renderer.Dsp
state.State3 = state.State2;
state.State2 = output;
outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
}
}
/// <summary>
/// Apply a single biquad filter and mix the result into the output buffer with volume ramp.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameter">The biquad filter parameter</param>
/// <param name="state">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Initial mix volume</param>
/// <param name="ramp">Volume increment step</param>
/// <returns>Last filtered sample value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float ProcessBiquadFilterAndMixRamp(
ref BiquadFilterParameter parameter,
ref BiquadFilterState state,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume,
float ramp)
{
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
float mixState = 0f;
for (int i = 0; i < sampleCount; i++)
{
float input = inputBuffer[i];
float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
state.State1 = state.State0;
state.State0 = input;
state.State3 = state.State2;
state.State2 = output;
mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
outputBuffer[i] += mixState;
volume += ramp;
}
return mixState;
}
/// <summary>
/// Apply multiple biquad filter.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessBiquadFilter(
ReadOnlySpan<BiquadFilterParameter> parameters,
Span<BiquadFilterState> states,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount)
{
for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++)
{
BiquadFilterParameter parameter = parameters[stageIndex];
ref BiquadFilterState state = ref states[stageIndex];
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
for (int i = 0; i < sampleCount; i++)
{
float input = stageIndex != 0 ? outputBuffer[i] : inputBuffer[i];
float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
state.State1 = state.State0;
state.State0 = input;
state.State3 = state.State2;
state.State2 = output;
outputBuffer[i] = output;
}
}
}
/// <summary>
/// Apply double biquad filter and mix the result into the output buffer.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Mix volume</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessDoubleBiquadFilterAndMix(
ref BiquadFilterParameter parameter0,
ref BiquadFilterParameter parameter1,
ref BiquadFilterState state0,
ref BiquadFilterState state1,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume)
{
float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
for (int i = 0; i < sampleCount; i++)
{
float input = inputBuffer[i];
float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
state0.State1 = state0.State0;
state0.State0 = input;
state0.State3 = state0.State2;
state0.State2 = output;
input = output;
output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
state1.State1 = state1.State0;
state1.State0 = input;
state1.State3 = state1.State2;
state1.State2 = output;
outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
}
}
/// <summary>
/// Apply double biquad filter and mix the result into the output buffer with volume ramp.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Initial mix volume</param>
/// <param name="ramp">Volume increment step</param>
/// <returns>Last filtered sample value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float ProcessDoubleBiquadFilterAndMixRamp(
ref BiquadFilterParameter parameter0,
ref BiquadFilterParameter parameter1,
ref BiquadFilterState state0,
ref BiquadFilterState state1,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume,
float ramp)
{
float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
float mixState = 0f;
for (int i = 0; i < sampleCount; i++)
{
float input = inputBuffer[i];
float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
state0.State1 = state0.State0;
state0.State0 = input;
state0.State3 = state0.State2;
state0.State2 = output;
input = output;
output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
state1.State1 = state1.State0;
state1.State0 = input;
state1.State3 = state1.State2;
state1.State2 = output;
mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
outputBuffer[i] += mixState;
volume += ramp;
}
return mixState;
}
}
}

View file

@ -0,0 +1,123 @@
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
public class BiquadFilterAndMixCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
public CommandType CommandType => CommandType.BiquadFilterAndMix;
public uint EstimatedProcessingTime { get; set; }
public ushort InputBufferIndex { get; }
public ushort OutputBufferIndex { get; }
private BiquadFilterParameter _parameter;
public Memory<BiquadFilterState> BiquadFilterState { get; }
public Memory<BiquadFilterState> PreviousBiquadFilterState { get; }
public Memory<VoiceUpdateState> State { get; }
public int LastSampleIndex { get; }
public float Volume0 { get; }
public float Volume1 { get; }
public bool NeedInitialization { get; }
public bool HasVolumeRamp { get; }
public bool IsFirstMixBuffer { get; }
public BiquadFilterAndMixCommand(
float volume0,
float volume1,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter,
Memory<BiquadFilterState> biquadFilterState,
Memory<BiquadFilterState> previousBiquadFilterState,
bool needInitialization,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
Enabled = true;
NodeId = nodeId;
InputBufferIndex = (ushort)inputBufferIndex;
OutputBufferIndex = (ushort)outputBufferIndex;
_parameter = filter;
BiquadFilterState = biquadFilterState;
PreviousBiquadFilterState = previousBiquadFilterState;
State = state;
LastSampleIndex = lastSampleIndex;
Volume0 = volume0;
Volume1 = volume1;
NeedInitialization = needInitialization;
HasVolumeRamp = hasVolumeRamp;
IsFirstMixBuffer = isFirstMixBuffer;
}
public void Process(CommandList context)
{
ReadOnlySpan<float> inputBuffer = context.GetBuffer(InputBufferIndex);
Span<float> outputBuffer = context.GetBuffer(OutputBufferIndex);
if (NeedInitialization)
{
// If there is no previous state, initialize to zero.
BiquadFilterState.Span[0] = new BiquadFilterState();
}
else if (IsFirstMixBuffer)
{
// This is the first buffer, set previous state to current state.
PreviousBiquadFilterState.Span[0] = BiquadFilterState.Span[0];
}
else
{
// Rewind the current state by copying back the previous state.
BiquadFilterState.Span[0] = PreviousBiquadFilterState.Span[0];
}
if (HasVolumeRamp)
{
float volume = Volume0;
float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessBiquadFilterAndMixRamp(
ref _parameter,
ref BiquadFilterState.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
volume,
ramp);
}
else
{
BiquadFilterHelper.ProcessBiquadFilterAndMix(
ref _parameter,
ref BiquadFilterState.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
Volume1);
}
}
}
}

View file

@ -30,8 +30,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
CopyMixBuffer,
LimiterVersion1,
LimiterVersion2,
GroupedBiquadFilter,
MultiTapBiquadFilter,
CaptureBuffer,
Compressor,
BiquadFilterAndMix,
MultiTapBiquadFilterAndMix,
}
}

View file

@ -24,7 +24,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public Memory<VoiceUpdateState> State { get; }
public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> volume0, Span<float> volume1, Memory<VoiceUpdateState> state, int nodeId)
public MixRampGroupedCommand(
uint mixBufferCount,
uint inputBufferIndex,
uint outputBufferIndex,
ReadOnlySpan<float> volume0,
ReadOnlySpan<float> volume1,
Memory<VoiceUpdateState> state,
int nodeId)
{
Enabled = true;
MixBufferCount = mixBufferCount;
@ -48,7 +55,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float ProcessMixRampGrouped(Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, float volume0, float volume1, int sampleCount)
private static float ProcessMixRampGrouped(
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
float volume0,
float volume1,
int sampleCount)
{
float ramp = (volume1 - volume0) / sampleCount;
float volume = volume0;

View file

@ -0,0 +1,145 @@
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
public class MultiTapBiquadFilterAndMixCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
public CommandType CommandType => CommandType.MultiTapBiquadFilterAndMix;
public uint EstimatedProcessingTime { get; set; }
public ushort InputBufferIndex { get; }
public ushort OutputBufferIndex { get; }
private BiquadFilterParameter _parameter0;
private BiquadFilterParameter _parameter1;
public Memory<BiquadFilterState> BiquadFilterState0 { get; }
public Memory<BiquadFilterState> BiquadFilterState1 { get; }
public Memory<BiquadFilterState> PreviousBiquadFilterState0 { get; }
public Memory<BiquadFilterState> PreviousBiquadFilterState1 { get; }
public Memory<VoiceUpdateState> State { get; }
public int LastSampleIndex { get; }
public float Volume0 { get; }
public float Volume1 { get; }
public bool NeedInitialization0 { get; }
public bool NeedInitialization1 { get; }
public bool HasVolumeRamp { get; }
public bool IsFirstMixBuffer { get; }
public MultiTapBiquadFilterAndMixCommand(
float volume0,
float volume1,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter0,
ref BiquadFilterParameter filter1,
Memory<BiquadFilterState> biquadFilterState0,
Memory<BiquadFilterState> biquadFilterState1,
Memory<BiquadFilterState> previousBiquadFilterState0,
Memory<BiquadFilterState> previousBiquadFilterState1,
bool needInitialization0,
bool needInitialization1,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
Enabled = true;
NodeId = nodeId;
InputBufferIndex = (ushort)inputBufferIndex;
OutputBufferIndex = (ushort)outputBufferIndex;
_parameter0 = filter0;
_parameter1 = filter1;
BiquadFilterState0 = biquadFilterState0;
BiquadFilterState1 = biquadFilterState1;
PreviousBiquadFilterState0 = previousBiquadFilterState0;
PreviousBiquadFilterState1 = previousBiquadFilterState1;
State = state;
LastSampleIndex = lastSampleIndex;
Volume0 = volume0;
Volume1 = volume1;
NeedInitialization0 = needInitialization0;
NeedInitialization1 = needInitialization1;
HasVolumeRamp = hasVolumeRamp;
IsFirstMixBuffer = isFirstMixBuffer;
}
private void UpdateState(Memory<BiquadFilterState> state, Memory<BiquadFilterState> previousState, bool needInitialization)
{
if (needInitialization)
{
// If there is no previous state, initialize to zero.
state.Span[0] = new BiquadFilterState();
}
else if (IsFirstMixBuffer)
{
// This is the first buffer, set previous state to current state.
previousState.Span[0] = state.Span[0];
}
else
{
// Rewind the current state by copying back the previous state.
state.Span[0] = previousState.Span[0];
}
}
public void Process(CommandList context)
{
ReadOnlySpan<float> inputBuffer = context.GetBuffer(InputBufferIndex);
Span<float> outputBuffer = context.GetBuffer(OutputBufferIndex);
UpdateState(BiquadFilterState0, PreviousBiquadFilterState0, NeedInitialization0);
UpdateState(BiquadFilterState1, PreviousBiquadFilterState1, NeedInitialization1);
if (HasVolumeRamp)
{
float volume = Volume0;
float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessDoubleBiquadFilterAndMixRamp(
ref _parameter0,
ref _parameter1,
ref BiquadFilterState0.Span[0],
ref BiquadFilterState1.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
volume,
ramp);
}
else
{
BiquadFilterHelper.ProcessDoubleBiquadFilterAndMix(
ref _parameter0,
ref _parameter1,
ref BiquadFilterState0.Span[0],
ref BiquadFilterState1.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
Volume1);
}
}
}
}

View file

@ -4,13 +4,13 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
public class GroupedBiquadFilterCommand : ICommand
public class MultiTapBiquadFilterCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
public CommandType CommandType => CommandType.GroupedBiquadFilter;
public CommandType CommandType => CommandType.MultiTapBiquadFilter;
public uint EstimatedProcessingTime { get; set; }
@ -20,7 +20,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private readonly int _outputBufferIndex;
private readonly bool[] _isInitialized;
public GroupedBiquadFilterCommand(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
public MultiTapBiquadFilterCommand(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
{
_parameters = filters.ToArray();
_biquadFilterStates = biquadFilterStateMemory;

View file

@ -2,12 +2,16 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)]
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x20)]
public struct BiquadFilterState
{
public float State0;
public float State1;
public float State2;
public float State3;
public float State4;
public float State5;
public float State6;
public float State7;
}
}

View file

@ -0,0 +1,43 @@
using Ryujinx.Common.Memory;
using System;
namespace Ryujinx.Audio.Renderer.Parameter
{
/// <summary>
/// Generic interface for the splitter destination parameters.
/// </summary>
public interface ISplitterDestinationInParameter
{
/// <summary>
/// Target splitter destination data id.
/// </summary>
int Id { get; }
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
int DestinationId { get; }
/// <summary>
/// Biquad filter parameters.
/// </summary>
Array2<BiquadFilterParameter> BiquadFilters { get; }
/// <summary>
/// Set to true if in use.
/// </summary>
bool IsUsed { get; }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
Span<float> MixBufferVolume { get; }
/// <summary>
/// Check if the magic is valid.
/// </summary>
/// <returns>Returns true if the magic is valid.</returns>
bool IsMagicValid();
}
}

View file

@ -1,3 +1,4 @@
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Runtime.InteropServices;
@ -5,10 +6,10 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Parameter
{
/// <summary>
/// Input header for a splitter destination update.
/// Input header for a splitter destination version 1 update.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SplitterDestinationInParameter
public struct SplitterDestinationInParameterVersion1 : ISplitterDestinationInParameter
{
/// <summary>
/// Magic of the input header.
@ -41,7 +42,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// </summary>
private unsafe fixed byte _reserved[3];
[StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary>
@ -50,6 +51,14 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume);
readonly int ISplitterDestinationInParameter.Id => Id;
readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => default;
readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
/// <summary>
/// The expected constant of any input header.
/// </summary>

View file

@ -0,0 +1,81 @@
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Parameter
{
/// <summary>
/// Input header for a splitter destination version 2 update.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SplitterDestinationInParameterVersion2 : ISplitterDestinationInParameter
{
/// <summary>
/// Magic of the input header.
/// </summary>
public uint Magic;
/// <summary>
/// Target splitter destination data id.
/// </summary>
public int Id;
/// <summary>
/// Mix buffer volumes storage.
/// </summary>
private MixArray _mixBufferVolume;
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
public int DestinationId;
/// <summary>
/// Biquad filter parameters.
/// </summary>
public Array2<BiquadFilterParameter> BiquadFilters;
/// <summary>
/// Set to true if in use.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
/// <summary>
/// Reserved/padding.
/// </summary>
private unsafe fixed byte _reserved[11];
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume);
readonly int ISplitterDestinationInParameter.Id => Id;
readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => BiquadFilters;
readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
/// <summary>
/// The expected constant of any input header.
/// </summary>
private const uint ValidMagic = 0x44444E53;
/// <summary>
/// Check if the magic is valid.
/// </summary>
/// <returns>Returns true if the magic is valid.</returns>
public readonly bool IsMagicValid()
{
return Magic == ValidMagic;
}
}
}

View file

@ -1,6 +1,7 @@
using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.Command;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Server.Effect;
using Ryujinx.Audio.Renderer.Server.MemoryPool;
@ -173,6 +174,22 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.WorkBufferTooSmall;
}
Memory<BiquadFilterState> splitterBqfStates = Memory<BiquadFilterState>.Empty;
if (_behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
parameter.SplitterCount > 0 &&
parameter.SplitterDestinationCount > 0)
{
splitterBqfStates = workBufferAllocator.Allocate<BiquadFilterState>(parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
if (splitterBqfStates.IsEmpty)
{
return ResultCode.WorkBufferTooSmall;
}
splitterBqfStates.Span.Clear();
}
// Invalidate DSP cache on what was currently allocated with workBuffer.
AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset);
@ -292,7 +309,7 @@ namespace Ryujinx.Audio.Renderer.Server
state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu);
}
if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator))
if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator, splitterBqfStates))
{
return ResultCode.WorkBufferTooSmall;
}
@ -775,6 +792,13 @@ namespace Ryujinx.Audio.Renderer.Server
// Splitter
size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter);
if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
parameter.SplitterCount > 0 &&
parameter.SplitterDestinationCount > 0)
{
size = WorkBufferAllocator.GetTargetSize<BiquadFilterState>(size, parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
}
// DSP Voice
size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align);

View file

@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Server
/// <see cref="Parameter.RendererInfoOutStatus"/> was added to supply the count of update done sent to the DSP.
/// A new version of the command estimator was added to address timing changes caused by the voice changes.
/// Additionally, the rendering limit percent was incremented to 80%.
///
/// </summary>
/// <remarks>This was added in system update 6.0.0</remarks>
public const int Revision5 = 5 << 24;
@ -101,10 +100,18 @@ namespace Ryujinx.Audio.Renderer.Server
/// <remarks>This was added in system update 14.0.0 but some changes were made in 15.0.0</remarks>
public const int Revision11 = 11 << 24;
/// <summary>
/// REV12:
/// Two new commands were added to for biquad filtering and mixing (with optinal volume ramp) on the same command.
/// Splitter destinations can now specify up to two biquad filtering parameters, used for filtering the buffer before mixing.
/// </summary>
/// <remarks>This was added in system update 17.0.0</remarks>
public const int Revision12 = 12 << 24;
/// <summary>
/// Last revision supported by the implementation.
/// </summary>
public const int LastRevision = Revision11;
public const int LastRevision = Revision12;
/// <summary>
/// Target revision magic supported by the implementation.
@ -212,7 +219,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// <summary>
/// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP.
/// </summary>
/// <returns>True if if the audio renderer should fix it.</returns>
/// <returns>True if the audio renderer should fix it.</returns>
public bool IsAdpcmLoopContextBugFixed()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2);
@ -354,7 +361,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// Check if the audio renderer should use an optimized Biquad Filter (Direct Form 1) in case of two biquad filters are defined on a voice.
/// </summary>
/// <returns>True if the audio renderer should use the optimization.</returns>
public bool IsBiquadFilterGroupedOptimizationSupported()
public bool UseMultiTapBiquadFilterProcessing()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision10);
}
@ -368,6 +375,15 @@ namespace Ryujinx.Audio.Renderer.Server
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision11);
}
/// <summary>
/// Check if the audio renderer should support biquad filter on splitter.
/// </summary>
/// <returns>True if the audio renderer support biquad filter on splitter</returns>
public bool IsBiquadFilterParameterForSplitterEnabled()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision12);
}
/// <summary>
/// Get the version of the <see cref="ICommandProcessingTimeEstimator"/>.
/// </summary>

View file

@ -204,7 +204,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
/// <summary>
/// Create a new <see cref="GroupedBiquadFilterCommand"/>.
/// Create a new <see cref="MultiTapBiquadFilterCommand"/>.
/// </summary>
/// <param name="baseIndex">The base index of the input and output buffer.</param>
/// <param name="filters">The biquad filter parameters.</param>
@ -213,9 +213,9 @@ namespace Ryujinx.Audio.Renderer.Server
/// <param name="outputBufferOffset">The output buffer offset.</param>
/// <param name="isInitialized">Set to true if the biquad filter state is initialized.</param>
/// <param name="nodeId">The node id associated to this command.</param>
public void GenerateGroupedBiquadFilter(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
public void GenerateMultiTapBiquadFilter(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
{
GroupedBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId);
MultiTapBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
@ -232,7 +232,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// <param name="volume">The new volume.</param>
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
/// <param name="nodeId">The node id associated to this command.</param>
public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> previousVolume, Span<float> volume, Memory<VoiceUpdateState> state, int nodeId)
public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, ReadOnlySpan<float> previousVolume, ReadOnlySpan<float> volume, Memory<VoiceUpdateState> state, int nodeId)
{
MixRampGroupedCommand command = new(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId);
@ -260,6 +260,120 @@ namespace Ryujinx.Audio.Renderer.Server
AddCommand(command);
}
/// <summary>
/// Generate a new <see cref="BiquadFilterAndMixCommand"/>.
/// </summary>
/// <param name="previousVolume">The previous volume.</param>
/// <param name="volume">The new volume.</param>
/// <param name="inputBufferIndex">The input buffer index.</param>
/// <param name="outputBufferIndex">The output buffer index.</param>
/// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param>
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
/// <param name="filter">The biquad filter parameter.</param>
/// <param name="biquadFilterState">The biquad state.</param>
/// <param name="previousBiquadFilterState">The previous biquad state.</param>
/// <param name="needInitialization">Set to true if the biquad filter state needs to be initialized.</param>
/// <param name="hasVolumeRamp">Set to true if the mix has volume ramp, and <paramref name="previousVolume"/> should be taken into account.</param>
/// <param name="isFirstMixBuffer">Set to true if the buffer is the first mix buffer.</param>
/// <param name="nodeId">The node id associated to this command.</param>
public void GenerateBiquadFilterAndMix(
float previousVolume,
float volume,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter,
Memory<BiquadFilterState> biquadFilterState,
Memory<BiquadFilterState> previousBiquadFilterState,
bool needInitialization,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
BiquadFilterAndMixCommand command = new(
previousVolume,
volume,
inputBufferIndex,
outputBufferIndex,
lastSampleIndex,
state,
ref filter,
biquadFilterState,
previousBiquadFilterState,
needInitialization,
hasVolumeRamp,
isFirstMixBuffer,
nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
AddCommand(command);
}
/// <summary>
/// Generate a new <see cref="MultiTapBiquadFilterAndMixCommand"/>.
/// </summary>
/// <param name="previousVolume">The previous volume.</param>
/// <param name="volume">The new volume.</param>
/// <param name="inputBufferIndex">The input buffer index.</param>
/// <param name="outputBufferIndex">The output buffer index.</param>
/// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param>
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
/// <param name="filter0">First biquad filter parameter.</param>
/// <param name="filter1">Second biquad filter parameter.</param>
/// <param name="biquadFilterState0">First biquad state.</param>
/// <param name="biquadFilterState1">Second biquad state.</param>
/// <param name="previousBiquadFilterState0">First previous biquad state.</param>
/// <param name="previousBiquadFilterState1">Second previous biquad state.</param>
/// <param name="needInitialization0">Set to true if the first biquad filter state needs to be initialized.</param>
/// <param name="needInitialization1">Set to true if the second biquad filter state needs to be initialized.</param>
/// <param name="hasVolumeRamp">Set to true if the mix has volume ramp, and <paramref name="previousVolume"/> should be taken into account.</param>
/// <param name="isFirstMixBuffer">Set to true if the buffer is the first mix buffer.</param>
/// <param name="nodeId">The node id associated to this command.</param>
public void GenerateMultiTapBiquadFilterAndMix(
float previousVolume,
float volume,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter0,
ref BiquadFilterParameter filter1,
Memory<BiquadFilterState> biquadFilterState0,
Memory<BiquadFilterState> biquadFilterState1,
Memory<BiquadFilterState> previousBiquadFilterState0,
Memory<BiquadFilterState> previousBiquadFilterState1,
bool needInitialization0,
bool needInitialization1,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
MultiTapBiquadFilterAndMixCommand command = new(
previousVolume,
volume,
inputBufferIndex,
outputBufferIndex,
lastSampleIndex,
state,
ref filter0,
ref filter1,
biquadFilterState0,
biquadFilterState1,
previousBiquadFilterState0,
previousBiquadFilterState1,
needInitialization0,
needInitialization1,
hasVolumeRamp,
isFirstMixBuffer,
nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
AddCommand(command);
}
/// <summary>
/// Generate a new <see cref="DepopForMixBuffersCommand"/>.
/// </summary>
@ -268,7 +382,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// <param name="bufferCount">The buffer count.</param>
/// <param name="nodeId">The node id associated to this command.</param>
/// <param name="sampleRate">The target sample rate in use.</param>
public void GenerateDepopForMixBuffersCommand(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
public void GenerateDepopForMixBuffers(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
{
DepopForMixBuffersCommand command = new(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate);

View file

@ -12,6 +12,7 @@ using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server
{
@ -46,7 +47,8 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(voiceState.MixId);
_commandBuffer.GenerateDepopPrepare(dspState,
_commandBuffer.GenerateDepopPrepare(
dspState,
_rendererContext.DepopBuffer,
mix.BufferCount,
mix.BufferOffset,
@ -59,15 +61,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true)
{
Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);
SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);
if (destinationSpan.IsEmpty)
if (destination.IsNull)
{
break;
}
ref SplitterDestination destination = ref destinationSpan[0];
if (destination.IsConfigured())
{
int mixId = destination.DestinationId;
@ -76,7 +76,8 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(mixId);
_commandBuffer.GenerateDepopPrepare(dspState,
_commandBuffer.GenerateDepopPrepare(
dspState,
_rendererContext.DepopBuffer,
mix.BufferCount,
mix.BufferOffset,
@ -95,7 +96,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported())
{
_commandBuffer.GenerateDataSourceVersion2(ref voiceState,
_commandBuffer.GenerateDataSourceVersion2(
ref voiceState,
dspState,
(ushort)_rendererContext.MixBufferCount,
(ushort)channelIndex,
@ -106,21 +108,24 @@ namespace Ryujinx.Audio.Renderer.Server
switch (voiceState.SampleFormat)
{
case SampleFormat.PcmInt16:
_commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState,
_commandBuffer.GeneratePcmInt16DataSourceVersion1(
ref voiceState,
dspState,
(ushort)_rendererContext.MixBufferCount,
(ushort)channelIndex,
voiceState.NodeId);
break;
case SampleFormat.PcmFloat:
_commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState,
_commandBuffer.GeneratePcmFloatDataSourceVersion1(
ref voiceState,
dspState,
(ushort)_rendererContext.MixBufferCount,
(ushort)channelIndex,
voiceState.NodeId);
break;
case SampleFormat.Adpcm:
_commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState,
_commandBuffer.GenerateAdpcmDataSourceVersion1(
ref voiceState,
dspState,
(ushort)_rendererContext.MixBufferCount,
voiceState.NodeId);
@ -134,14 +139,14 @@ namespace Ryujinx.Audio.Renderer.Server
private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory<VoiceUpdateState> state, int baseIndex, int bufferOffset, int nodeId)
{
bool supportsOptimizedPath = _rendererContext.BehaviourContext.IsBiquadFilterGroupedOptimizationSupported();
bool supportsOptimizedPath = _rendererContext.BehaviourContext.UseMultiTapBiquadFilterProcessing();
if (supportsOptimizedPath && voiceState.BiquadFilters[0].Enable && voiceState.BiquadFilters[1].Enable)
{
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(Unsafe.SizeOf<BiquadFilterState>() * Constants.VoiceBiquadFilterCount)];
Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory);
_commandBuffer.GenerateGroupedBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
_commandBuffer.GenerateMultiTapBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
}
else
{
@ -151,11 +156,11 @@ namespace Ryujinx.Audio.Renderer.Server
if (filter.Enable)
{
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(Unsafe.SizeOf<BiquadFilterState>() * Constants.VoiceBiquadFilterCount)];
Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory);
_commandBuffer.GenerateBiquadFilter(baseIndex,
_commandBuffer.GenerateBiquadFilter(
baseIndex,
ref filter,
stateMemory.Slice(i, 1),
bufferOffset,
@ -167,11 +172,112 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
private void GenerateVoiceMix(Span<float> mixVolumes, Span<float> previousMixVolumes, Memory<VoiceUpdateState> state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId)
private void GenerateVoiceMixWithSplitter(
SplitterDestination destination,
Memory<VoiceUpdateState> state,
uint bufferOffset,
uint bufferCount,
uint bufferIndex,
int nodeId)
{
ReadOnlySpan<float> mixVolumes = destination.MixBufferVolume;
ReadOnlySpan<float> previousMixVolumes = destination.PreviousMixBufferVolume;
ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
Memory<BiquadFilterState> bqfState = _splitterContext.GetBiquadFilterState(destination);
bool isFirstMixBuffer = true;
for (int i = 0; i < bufferCount; i++)
{
float previousMixVolume = previousMixVolumes[i];
float mixVolume = mixVolumes[i];
if (mixVolume != 0.0f || previousMixVolume != 0.0f)
{
if (bqf0.Enable && bqf1.Enable)
{
_commandBuffer.GenerateMultiTapBiquadFilterAndMix(
previousMixVolume,
mixVolume,
bufferIndex,
bufferOffset + (uint)i,
i,
state,
ref bqf0,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
bqfState.Slice(2, 1),
bqfState.Slice(3, 1),
!destination.IsBiquadFilterEnabledPrev(),
!destination.IsBiquadFilterEnabledPrev(),
true,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
destination.UpdateBiquadFilterEnabledPrev(1);
}
else if (bqf0.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
previousMixVolume,
mixVolume,
bufferIndex,
bufferOffset + (uint)i,
i,
state,
ref bqf0,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
true,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
}
else if (bqf1.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
previousMixVolume,
mixVolume,
bufferIndex,
bufferOffset + (uint)i,
i,
state,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
true,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(1);
}
isFirstMixBuffer = false;
}
}
}
private void GenerateVoiceMix(
ReadOnlySpan<float> mixVolumes,
ReadOnlySpan<float> previousMixVolumes,
Memory<VoiceUpdateState> state,
uint bufferOffset,
uint bufferCount,
uint bufferIndex,
int nodeId)
{
if (bufferCount > Constants.VoiceChannelCountMax)
{
_commandBuffer.GenerateMixRampGrouped(bufferCount,
_commandBuffer.GenerateMixRampGrouped(
bufferCount,
bufferIndex,
bufferOffset,
previousMixVolumes,
@ -188,7 +294,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (mixVolume != 0.0f || previousMixVolume != 0.0f)
{
_commandBuffer.GenerateMixRamp(previousMixVolume,
_commandBuffer.GenerateMixRamp(
previousMixVolume,
mixVolume,
bufferIndex,
bufferOffset + (uint)i,
@ -271,7 +378,8 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
_commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume,
_commandBuffer.GenerateVolumeRamp(
voiceState.PreviousVolume,
voiceState.Volume,
_rendererContext.MixBufferCount + (uint)channelIndex,
nodeId);
@ -291,15 +399,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true)
{
Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);
SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);
if (destinationSpan.IsEmpty)
if (destination.IsNull)
{
break;
}
ref SplitterDestination destination = ref destinationSpan[0];
destinationId += (int)channelsCount;
if (destination.IsConfigured())
@ -310,13 +416,27 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(mixId);
GenerateVoiceMix(destination.MixBufferVolume,
if (destination.IsBiquadFilterEnabled())
{
GenerateVoiceMixWithSplitter(
destination,
dspStateMemory,
mix.BufferOffset,
mix.BufferCount,
_rendererContext.MixBufferCount + (uint)channelIndex,
nodeId);
}
else
{
GenerateVoiceMix(
destination.MixBufferVolume,
destination.PreviousMixBufferVolume,
dspStateMemory,
mix.BufferOffset,
mix.BufferCount,
_rendererContext.MixBufferCount + (uint)channelIndex,
nodeId);
}
destination.MarkAsNeedToUpdateInternalState();
}
@ -337,7 +457,8 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
GenerateVoiceMix(channelResource.Mix.AsSpan(),
GenerateVoiceMix(
channelResource.Mix.AsSpan(),
channelResource.PreviousMix.AsSpan(),
dspStateMemory,
mix.BufferOffset,
@ -409,7 +530,8 @@ namespace Ryujinx.Audio.Renderer.Server
{
if (effect.Parameter.Volumes[i] != 0.0f)
{
_commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i],
_commandBuffer.GenerateMix(
(uint)bufferOffset + effect.Parameter.Input[i],
(uint)bufferOffset + effect.Parameter.Output[i],
nodeId,
effect.Parameter.Volumes[i]);
@ -447,7 +569,8 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount;
}
_commandBuffer.GenerateAuxEffect(bufferOffset,
_commandBuffer.GenerateAuxEffect(
bufferOffset,
effect.Parameter.Input[i],
effect.Parameter.Output[i],
ref effect.State,
@ -512,7 +635,10 @@ namespace Ryujinx.Audio.Renderer.Server
for (int i = 0; i < effect.Parameter.ChannelCount; i++)
{
_commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1),
_commandBuffer.GenerateBiquadFilter(
(int)bufferOffset,
ref parameter,
effect.State.Slice(i, 1),
effect.Parameter.Input[i],
effect.Parameter.Output[i],
needInitialization,
@ -591,7 +717,8 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount;
}
_commandBuffer.GenerateCaptureEffect(bufferOffset,
_commandBuffer.GenerateCaptureEffect(
bufferOffset,
effect.Parameter.Input[i],
effect.State.SendBufferInfo,
effect.IsEnabled,
@ -612,7 +739,8 @@ namespace Ryujinx.Audio.Renderer.Server
{
Debug.Assert(effect.Type == EffectType.Compressor);
_commandBuffer.GenerateCompressorEffect(bufferOffset,
_commandBuffer.GenerateCompressorEffect(
bufferOffset,
effect.Parameter,
effect.State,
effect.IsEnabled,
@ -629,8 +757,11 @@ namespace Ryujinx.Audio.Renderer.Server
bool performanceInitialized = false;
if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, effect.GetPerformanceDetailType(),
isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, nodeId))
if (_performanceManager != null && _performanceManager.GetNextEntry(
out performanceEntry,
effect.GetPerformanceDetailType(),
isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix,
nodeId))
{
performanceInitialized = true;
@ -706,6 +837,85 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
private void GenerateMixWithSplitter(
uint inputBufferIndex,
uint outputBufferIndex,
float volume,
SplitterDestination destination,
ref bool isFirstMixBuffer,
int nodeId)
{
ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
Memory<BiquadFilterState> bqfState = _splitterContext.GetBiquadFilterState(destination);
if (bqf0.Enable && bqf1.Enable)
{
_commandBuffer.GenerateMultiTapBiquadFilterAndMix(
0f,
volume,
inputBufferIndex,
outputBufferIndex,
0,
Memory<VoiceUpdateState>.Empty,
ref bqf0,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
bqfState.Slice(2, 1),
bqfState.Slice(3, 1),
!destination.IsBiquadFilterEnabledPrev(),
!destination.IsBiquadFilterEnabledPrev(),
false,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
destination.UpdateBiquadFilterEnabledPrev(1);
}
else if (bqf0.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
0f,
volume,
inputBufferIndex,
outputBufferIndex,
0,
Memory<VoiceUpdateState>.Empty,
ref bqf0,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
false,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
}
else if (bqf1.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
0f,
volume,
inputBufferIndex,
outputBufferIndex,
0,
Memory<VoiceUpdateState>.Empty,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
false,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(1);
}
isFirstMixBuffer = false;
}
private void GenerateMix(ref MixState mix)
{
if (mix.HasAnyDestination())
@ -722,15 +932,13 @@ namespace Ryujinx.Audio.Renderer.Server
{
int destinationIndex = destinationId++;
Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);
SplitterDestination destination = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);
if (destinationSpan.IsEmpty)
if (destination.IsNull)
{
break;
}
ref SplitterDestination destination = ref destinationSpan[0];
if (destination.IsConfigured())
{
int mixId = destination.DestinationId;
@ -741,13 +949,28 @@ namespace Ryujinx.Audio.Renderer.Server
uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount);
bool isFirstMixBuffer = true;
for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++)
{
float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex);
if (volume != 0.0f)
{
_commandBuffer.GenerateMix(inputBufferIndex,
if (destination.IsBiquadFilterEnabled())
{
GenerateMixWithSplitter(
inputBufferIndex,
destinationMix.BufferOffset + bufferDestinationIndex,
volume,
destination,
ref isFirstMixBuffer,
mix.NodeId);
}
else
{
_commandBuffer.GenerateMix(
inputBufferIndex,
destinationMix.BufferOffset + bufferDestinationIndex,
mix.NodeId,
volume);
@ -758,6 +981,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
}
}
else
{
ref MixState destinationMix = ref _mixContext.GetState(mix.DestinationMixId);
@ -770,7 +994,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (volume != 0.0f)
{
_commandBuffer.GenerateMix(mix.BufferOffset + bufferIndex,
_commandBuffer.GenerateMix(
mix.BufferOffset + bufferIndex,
destinationMix.BufferOffset + bufferDestinationIndex,
mix.NodeId,
volume);
@ -783,7 +1008,8 @@ namespace Ryujinx.Audio.Renderer.Server
private void GenerateSubMix(ref MixState subMix)
{
_commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer,
_commandBuffer.GenerateDepopForMixBuffers(
_rendererContext.DepopBuffer,
subMix.BufferOffset,
subMix.BufferCount,
subMix.NodeId,
@ -847,7 +1073,8 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState finalMix = ref _mixContext.GetFinalState();
_commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer,
_commandBuffer.GenerateDepopForMixBuffers(
_rendererContext.DepopBuffer,
finalMix.BufferOffset,
finalMix.BufferCount,
finalMix.NodeId,
@ -882,7 +1109,8 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
_commandBuffer.GenerateVolume(finalMix.Volume,
_commandBuffer.GenerateVolume(
finalMix.Volume,
finalMix.BufferOffset + bufferIndex,
nodeId);
@ -938,7 +1166,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (useCustomDownMixingCommand)
{
_commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
_commandBuffer.GenerateDownMixSurroundToStereo(
finalMix.BufferOffset,
sink.Parameter.Input.AsSpan(),
sink.Parameter.Input.AsSpan(),
sink.DownMixCoefficients,
@ -947,7 +1176,8 @@ namespace Ryujinx.Audio.Renderer.Server
// NOTE: We do the downmixing at the DSP level as it's easier that way.
else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6)
{
_commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
_commandBuffer.GenerateDownMixSurroundToStereo(
finalMix.BufferOffset,
sink.Parameter.Input.AsSpan(),
sink.Parameter.Input.AsSpan(),
Constants.DefaultSurroundToStereoCoefficients,
@ -958,7 +1188,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (sink.UpsamplerState != null)
{
_commandBuffer.GenerateUpsample(finalMix.BufferOffset,
_commandBuffer.GenerateUpsample(
finalMix.BufferOffset,
sink.UpsamplerState,
sink.Parameter.InputCount,
sink.Parameter.Input.AsSpan(),
@ -968,7 +1199,8 @@ namespace Ryujinx.Audio.Renderer.Server
Constants.InvalidNodeId);
}
_commandBuffer.GenerateDeviceSink(finalMix.BufferOffset,
_commandBuffer.GenerateDeviceSink(
finalMix.BufferOffset,
sink,
_rendererContext.SessionId,
commandList.Buffers,

View file

@ -170,7 +170,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0;
}
public uint Estimate(GroupedBiquadFilterCommand command)
public uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@ -184,5 +184,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
public uint Estimate(BiquadFilterAndMixCommand command)
{
return 0;
}
public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
return 0;
}
}
}

View file

@ -462,7 +462,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0;
}
public uint Estimate(GroupedBiquadFilterCommand command)
public uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@ -476,5 +476,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
public uint Estimate(BiquadFilterAndMixCommand command)
{
return 0;
}
public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
return 0;
}
}
}

View file

@ -632,7 +632,7 @@ namespace Ryujinx.Audio.Renderer.Server
};
}
public virtual uint Estimate(GroupedBiquadFilterCommand command)
public virtual uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@ -646,5 +646,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
public virtual uint Estimate(BiquadFilterAndMixCommand command)
{
return 0;
}
public virtual uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
return 0;
}
}
}

View file

@ -10,7 +10,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
public CommandProcessingTimeEstimatorVersion4(uint sampleCount, uint bufferCount) : base(sampleCount, bufferCount) { }
public override uint Estimate(GroupedBiquadFilterCommand command)
public override uint Estimate(MultiTapBiquadFilterCommand command)
{
Debug.Assert(SampleCount == 160 || SampleCount == 240);

View file

@ -210,5 +210,53 @@ namespace Ryujinx.Audio.Renderer.Server
_ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
};
}
public override uint Estimate(BiquadFilterAndMixCommand command)
{
Debug.Assert(SampleCount == 160 || SampleCount == 240);
if (command.HasVolumeRamp)
{
if (SampleCount == 160)
{
return 5204;
}
return 6683;
}
else
{
if (SampleCount == 160)
{
return 3427;
}
return 4752;
}
}
public override uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
Debug.Assert(SampleCount == 160 || SampleCount == 240);
if (command.HasVolumeRamp)
{
if (SampleCount == 160)
{
return 7939;
}
return 10669;
}
else
{
if (SampleCount == 160)
{
return 6256;
}
return 8683;
}
}
}
}

View file

@ -33,8 +33,10 @@ namespace Ryujinx.Audio.Renderer.Server
uint Estimate(UpsampleCommand command);
uint Estimate(LimiterCommandVersion1 command);
uint Estimate(LimiterCommandVersion2 command);
uint Estimate(GroupedBiquadFilterCommand command);
uint Estimate(MultiTapBiquadFilterCommand command);
uint Estimate(CaptureBufferCommand command);
uint Estimate(CompressorCommand command);
uint Estimate(BiquadFilterAndMixCommand command);
uint Estimate(MultiTapBiquadFilterAndMixCommand command);
}
}

View file

@ -225,11 +225,11 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
for (int i = 0; i < splitter.DestinationCount; i++)
{
Span<SplitterDestination> destination = splitter.GetData(i);
SplitterDestination destination = splitter.GetData(i);
if (!destination.IsEmpty)
if (!destination.IsNull)
{
int destinationMixId = destination[0].DestinationId;
int destinationMixId = destination.DestinationId;
if (destinationMixId != UnusedMixId)
{

View file

@ -1,4 +1,5 @@
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common;
@ -15,15 +16,35 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
public class SplitterContext
{
/// <summary>
/// Amount of biquad filter states per splitter destination.
/// </summary>
public const int BqfStatesPerDestination = 4;
/// <summary>
/// Storage for <see cref="SplitterState"/>.
/// </summary>
private Memory<SplitterState> _splitters;
/// <summary>
/// Storage for <see cref="SplitterDestination"/>.
/// Storage for <see cref="SplitterDestinationVersion1"/>.
/// </summary>
private Memory<SplitterDestination> _splitterDestinations;
private Memory<SplitterDestinationVersion1> _splitterDestinationsV1;
/// <summary>
/// Storage for <see cref="SplitterDestinationVersion2"/>.
/// </summary>
private Memory<SplitterDestinationVersion2> _splitterDestinationsV2;
/// <summary>
/// Splitter biquad filtering states.
/// </summary>
private Memory<BiquadFilterState> _splitterBqfStates;
/// <summary>
/// Version of the splitter context that is being used, currently can be 1 or 2.
/// </summary>
public int Version { get; private set; }
/// <summary>
/// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.
@ -36,12 +57,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// <param name="behaviourContext">The behaviour context.</param>
/// <param name="parameter">The audio renderer configuration.</param>
/// <param name="workBufferAllocator">The <see cref="WorkBufferAllocator"/>.</param>
/// <param name="splitterBqfStates">Memory to store the biquad filtering state for splitters during processing.</param>
/// <returns>Return true if the initialization was successful.</returns>
public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator)
public bool Initialize(
ref BehaviourContext behaviourContext,
ref AudioRendererConfiguration parameter,
WorkBufferAllocator workBufferAllocator,
Memory<BiquadFilterState> splitterBqfStates)
{
if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0)
{
Setup(Memory<SplitterState>.Empty, Memory<SplitterDestination>.Empty, false);
Setup(Memory<SplitterState>.Empty, Memory<SplitterDestinationVersion1>.Empty, Memory<SplitterDestinationVersion2>.Empty, false);
return true;
}
@ -60,23 +86,62 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
splitter = new SplitterState(splitterId++);
}
Memory<SplitterDestination> splitterDestinations = workBufferAllocator.Allocate<SplitterDestination>(parameter.SplitterDestinationCount,
SplitterDestination.Alignment);
Memory<SplitterDestinationVersion1> splitterDestinationsV1 = Memory<SplitterDestinationVersion1>.Empty;
Memory<SplitterDestinationVersion2> splitterDestinationsV2 = Memory<SplitterDestinationVersion2>.Empty;
if (splitterDestinations.IsEmpty)
if (!behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
{
Version = 1;
splitterDestinationsV1 = workBufferAllocator.Allocate<SplitterDestinationVersion1>(parameter.SplitterDestinationCount,
SplitterDestinationVersion1.Alignment);
if (splitterDestinationsV1.IsEmpty)
{
return false;
}
int splitterDestinationId = 0;
foreach (ref SplitterDestination data in splitterDestinations.Span)
foreach (ref SplitterDestinationVersion1 data in splitterDestinationsV1.Span)
{
data = new SplitterDestination(splitterDestinationId++);
data = new SplitterDestinationVersion1(splitterDestinationId++);
}
}
else
{
Version = 2;
splitterDestinationsV2 = workBufferAllocator.Allocate<SplitterDestinationVersion2>(parameter.SplitterDestinationCount,
SplitterDestinationVersion2.Alignment);
if (splitterDestinationsV2.IsEmpty)
{
return false;
}
int splitterDestinationId = 0;
foreach (ref SplitterDestinationVersion2 data in splitterDestinationsV2.Span)
{
data = new SplitterDestinationVersion2(splitterDestinationId++);
}
if (parameter.SplitterDestinationCount > 0)
{
// Official code stores it in the SplitterDestinationVersion2 struct,
// but we don't to avoid using unsafe code.
splitterBqfStates.Span.Clear();
_splitterBqfStates = splitterBqfStates;
}
else
{
_splitterBqfStates = Memory<BiquadFilterState>.Empty;
}
}
SplitterState.InitializeSplitters(splitters.Span);
Setup(splitters, splitterDestinations, behaviourContext.IsSplitterBugFixed());
Setup(splitters, splitterDestinationsV1, splitterDestinationsV2, behaviourContext.IsSplitterBugFixed());
return true;
}
@ -93,7 +158,15 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
if (behaviourContext.IsSplitterSupported())
{
size = WorkBufferAllocator.GetTargetSize<SplitterState>(size, parameter.SplitterCount, SplitterState.Alignment);
size = WorkBufferAllocator.GetTargetSize<SplitterDestination>(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment);
if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
{
size = WorkBufferAllocator.GetTargetSize<SplitterDestinationVersion2>(size, parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment);
}
else
{
size = WorkBufferAllocator.GetTargetSize<SplitterDestinationVersion1>(size, parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment);
}
if (behaviourContext.IsSplitterBugFixed())
{
@ -110,12 +183,18 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Setup the <see cref="SplitterContext"/> instance.
/// </summary>
/// <param name="splitters">The <see cref="SplitterState"/> storage.</param>
/// <param name="splitterDestinations">The <see cref="SplitterDestination"/> storage.</param>
/// <param name="splitterDestinationsV1">The <see cref="SplitterDestinationVersion1"/> storage.</param>
/// <param name="splitterDestinationsV2">The <see cref="SplitterDestinationVersion2"/> storage.</param>
/// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.</param>
private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed)
private void Setup(
Memory<SplitterState> splitters,
Memory<SplitterDestinationVersion1> splitterDestinationsV1,
Memory<SplitterDestinationVersion2> splitterDestinationsV2,
bool isBugFixed)
{
_splitters = splitters;
_splitterDestinations = splitterDestinations;
_splitterDestinationsV1 = splitterDestinationsV1;
_splitterDestinationsV2 = splitterDestinationsV2;
IsBugFixed = isBugFixed;
}
@ -141,7 +220,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
return 0;
}
return _splitterDestinations.Length / _splitters.Length;
int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length;
return length / _splitters.Length;
}
/// <summary>
@ -178,7 +259,39 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
/// <summary>
/// Update one or multiple <see cref="SplitterDestination"/> from user parameters.
/// Update one splitter destination data from user parameters.
/// </summary>
/// <param name="input">The raw data after the splitter header.</param>
/// <returns>True if the update was successful, false otherwise</returns>
private bool UpdateData<T>(ref SequenceReader<byte> input) where T : unmanaged, ISplitterDestinationInParameter
{
ref readonly T parameter = ref input.GetRefOrRefToCopy<T>(out _);
Debug.Assert(parameter.IsMagicValid());
if (parameter.IsMagicValid())
{
int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length;
if (parameter.Id >= 0 && parameter.Id < length)
{
SplitterDestination destination = GetDestination(parameter.Id);
destination.Update(parameter);
}
return true;
}
else
{
input.Rewind(Unsafe.SizeOf<T>());
return false;
}
}
/// <summary>
/// Update one or multiple splitter destination data from user parameters.
/// </summary>
/// <param name="inputHeader">The splitter header.</param>
/// <param name="input">The raw data after the splitter header.</param>
@ -186,23 +299,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
{
ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy<SplitterDestinationInParameter>(out _);
Debug.Assert(parameter.IsMagicValid());
if (parameter.IsMagicValid())
if (Version == 1)
{
if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length)
if (!UpdateData<SplitterDestinationInParameterVersion1>(ref input))
{
ref SplitterDestination destination = ref GetDestination(parameter.Id);
destination.Update(parameter);
break;
}
}
else if (Version == 2)
{
if (!UpdateData<SplitterDestinationInParameterVersion2>(ref input))
{
break;
}
}
else
{
input.Rewind(Unsafe.SizeOf<SplitterDestinationInParameter>());
break;
Debug.Fail($"Invalid splitter context version {Version}.");
}
}
}
@ -214,7 +327,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// <returns>Return true if the update was successful.</returns>
public bool Update(ref SequenceReader<byte> input)
{
if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
if (!UsingSplitter())
{
return true;
}
@ -251,45 +364,52 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
/// <summary>
/// Get a reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>.
/// Get a reference to the splitter destination data at the given <paramref name="id"/>.
/// </summary>
/// <param name="id">The index to use.</param>
/// <returns>A reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>.</returns>
public ref SplitterDestination GetDestination(int id)
/// <returns>A reference to the splitter destination data at the given <paramref name="id"/>.</returns>
public SplitterDestination GetDestination(int id)
{
return ref SpanIOHelper.GetFromMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
if (_splitterDestinationsV2.IsEmpty)
{
return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV1, id, (uint)_splitterDestinationsV1.Length));
}
else
{
return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV2, id, (uint)_splitterDestinationsV2.Length));
}
}
/// <summary>
/// Get a <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>.
/// </summary>
/// <param name="id">The index to use.</param>
/// <returns>A <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>.</returns>
public Memory<SplitterDestination> GetDestinationMemory(int id)
{
return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
}
/// <summary>
/// Get a <see cref="Span{SplitterDestination}"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>.
/// Get a <see cref="SplitterDestination"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>.
/// </summary>
/// <param name="id">The index to use to get the <see cref="SplitterState"/>.</param>
/// <param name="destinationId">The index of the <see cref="SplitterDestination"/>.</param>
/// <returns>A <see cref="Span{SplitterDestination}"/>.</returns>
public Span<SplitterDestination> GetDestination(int id, int destinationId)
/// <returns>A <see cref="SplitterDestination"/>.</returns>
public SplitterDestination GetDestination(int id, int destinationId)
{
ref SplitterState splitter = ref GetState(id);
return splitter.GetData(destinationId);
}
/// <summary>
/// Gets the biquad filter state for a given splitter destination.
/// </summary>
/// <param name="destination">The splitter destination.</param>
/// <returns>Biquad filter state for the specified destination.</returns>
public Memory<BiquadFilterState> GetBiquadFilterState(SplitterDestination destination)
{
return _splitterBqfStates.Slice(destination.Id * BqfStatesPerDestination, BqfStatesPerDestination);
}
/// <summary>
/// Return true if the audio renderer has any splitters.
/// </summary>
/// <returns>True if the audio renderer has any splitters.</returns>
public bool UsingSplitter()
{
return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty;
return !_splitters.IsEmpty && (!_splitterDestinationsV1.IsEmpty || !_splitterDestinationsV2.IsEmpty);
}
/// <summary>

View file

@ -1,115 +1,198 @@
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Utilities;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
/// <summary>
/// Server state for a splitter destination.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
public struct SplitterDestination
public ref struct SplitterDestination
{
public const int Alignment = 0x10;
private ref SplitterDestinationVersion1 _v1;
private ref SplitterDestinationVersion2 _v2;
/// <summary>
/// The unique id of this <see cref="SplitterDestination"/>.
/// Checks if the splitter destination data reference is null.
/// </summary>
public int Id;
public bool IsNull => Unsafe.IsNullRef(ref _v1) && Unsafe.IsNullRef(ref _v2);
/// <summary>
/// The splitter unique id.
/// </summary>
public int Id
{
get
{
if (Unsafe.IsNullRef(ref _v2))
{
if (Unsafe.IsNullRef(ref _v1))
{
return 0;
}
else
{
return _v1.Id;
}
}
else
{
return _v2.Id;
}
}
}
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
public int DestinationId;
/// <summary>
/// Mix buffer volumes storage.
/// </summary>
private MixArray _mix;
private MixArray _previousMix;
/// <summary>
/// Pointer to the next linked element.
/// </summary>
private unsafe SplitterDestination* _next;
/// <summary>
/// Set to true if in use.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
/// <summary>
/// Set to true if the internal state need to be updated.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool NeedToUpdateInternalState;
[StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
public int DestinationId
{
get
{
if (Unsafe.IsNullRef(ref _v2))
{
if (Unsafe.IsNullRef(ref _v1))
{
return 0;
}
else
{
return _v1.DestinationId;
}
}
else
{
return _v2.DestinationId;
}
}
}
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
public Span<float> MixBufferVolume
{
get
{
if (Unsafe.IsNullRef(ref _v2))
{
if (Unsafe.IsNullRef(ref _v1))
{
return Span<float>.Empty;
}
else
{
return _v1.MixBufferVolume;
}
}
else
{
return _v2.MixBufferVolume;
}
}
}
/// <summary>
/// Previous mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
public Span<float> PreviousMixBufferVolume
{
get
{
if (Unsafe.IsNullRef(ref _v2))
{
if (Unsafe.IsNullRef(ref _v1))
{
return Span<float>.Empty;
}
else
{
return _v1.PreviousMixBufferVolume;
}
}
else
{
return _v2.PreviousMixBufferVolume;
}
}
}
/// <summary>
/// Get the <see cref="Span{SplitterDestination}"/> of the next element or <see cref="Span{SplitterDestination}.Empty"/> if not present.
/// Get the <see cref="SplitterDestination"/> of the next element or null if not present.
/// </summary>
public readonly Span<SplitterDestination> Next
public readonly SplitterDestination Next
{
get
{
unsafe
{
return _next != null ? new Span<SplitterDestination>(_next, 1) : Span<SplitterDestination>.Empty;
}
}
}
/// <summary>
/// Create a new <see cref="SplitterDestination"/>.
/// </summary>
/// <param name="id">The unique id of this <see cref="SplitterDestination"/>.</param>
public SplitterDestination(int id) : this()
if (Unsafe.IsNullRef(ref _v2))
{
Id = id;
DestinationId = Constants.UnusedMixId;
ClearVolumes();
if (Unsafe.IsNullRef(ref _v1))
{
return new SplitterDestination();
}
else
{
return new SplitterDestination(ref _v1.Next);
}
}
else
{
return new SplitterDestination(ref _v2.Next);
}
}
}
}
/// <summary>
/// Update the <see cref="SplitterDestination"/> from user parameter.
/// Creates a new splitter destination wrapper for the version 1 splitter destination data.
/// </summary>
/// <param name="v1">Version 1 splitter destination data</param>
public SplitterDestination(ref SplitterDestinationVersion1 v1)
{
_v1 = ref v1;
_v2 = ref Unsafe.NullRef<SplitterDestinationVersion2>();
}
/// <summary>
/// Creates a new splitter destination wrapper for the version 2 splitter destination data.
/// </summary>
/// <param name="v2">Version 2 splitter destination data</param>
public SplitterDestination(ref SplitterDestinationVersion2 v2)
{
_v1 = ref Unsafe.NullRef<SplitterDestinationVersion1>();
_v2 = ref v2;
}
/// <summary>
/// Creates a new splitter destination wrapper for the splitter destination data.
/// </summary>
/// <param name="v1">Version 1 splitter destination data</param>
/// <param name="v2">Version 2 splitter destination data</param>
public unsafe SplitterDestination(SplitterDestinationVersion1* v1, SplitterDestinationVersion2* v2)
{
_v1 = ref Unsafe.AsRef<SplitterDestinationVersion1>(v1);
_v2 = ref Unsafe.AsRef<SplitterDestinationVersion2>(v2);
}
/// <summary>
/// Update the splitter destination data from user parameter.
/// </summary>
/// <param name="parameter">The user parameter.</param>
public void Update(SplitterDestinationInParameter parameter)
public void Update<T>(in T parameter) where T : ISplitterDestinationInParameter
{
Debug.Assert(Id == parameter.Id);
if (parameter.IsMagicValid() && Id == parameter.Id)
if (Unsafe.IsNullRef(ref _v2))
{
DestinationId = parameter.DestinationId;
parameter.MixBufferVolume.CopyTo(MixBufferVolume);
if (!IsUsed && parameter.IsUsed)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
NeedToUpdateInternalState = false;
_v1.Update(parameter);
}
IsUsed = parameter.IsUsed;
else
{
_v2.Update(parameter);
}
}
@ -118,12 +201,14 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
public void UpdateInternalState()
{
if (IsUsed && NeedToUpdateInternalState)
if (Unsafe.IsNullRef(ref _v2))
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
_v1.UpdateInternalState();
}
else
{
_v2.UpdateInternalState();
}
NeedToUpdateInternalState = false;
}
/// <summary>
@ -131,16 +216,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
public void MarkAsNeedToUpdateInternalState()
{
NeedToUpdateInternalState = true;
if (Unsafe.IsNullRef(ref _v2))
{
_v1.MarkAsNeedToUpdateInternalState();
}
else
{
_v2.MarkAsNeedToUpdateInternalState();
}
}
/// <summary>
/// Return true if the <see cref="SplitterDestination"/> is used and has a destination.
/// Return true if the splitter destination is used and has a destination.
/// </summary>
/// <returns>True if the <see cref="SplitterDestination"/> is used and has a destination.</returns>
/// <returns>True if the splitter destination is used and has a destination.</returns>
public readonly bool IsConfigured()
{
return IsUsed && DestinationId != Constants.UnusedMixId;
return Unsafe.IsNullRef(ref _v2) ? _v1.IsConfigured() : _v2.IsConfigured();
}
/// <summary>
@ -150,9 +242,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// <returns>The volume for the given destination.</returns>
public float GetMixVolume(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolume(destinationIndex) : _v2.GetMixVolume(destinationIndex);
}
return MixBufferVolume[destinationIndex];
/// <summary>
/// Get the previous volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolumePrev(int destinationIndex)
{
return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolumePrev(destinationIndex) : _v2.GetMixVolumePrev(destinationIndex);
}
/// <summary>
@ -160,22 +260,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
public void ClearVolumes()
{
MixBufferVolume.Clear();
PreviousMixBufferVolume.Clear();
if (Unsafe.IsNullRef(ref _v2))
{
_v1.ClearVolumes();
}
else
{
_v2.ClearVolumes();
}
}
/// <summary>
/// Link the next element to the given <see cref="SplitterDestination"/>.
/// Link the next element to the given splitter destination.
/// </summary>
/// <param name="next">The given <see cref="SplitterDestination"/> to link.</param>
public void Link(ref SplitterDestination next)
/// <param name="next">The given splitter destination to link.</param>
public void Link(SplitterDestination next)
{
unsafe
if (Unsafe.IsNullRef(ref _v2))
{
fixed (SplitterDestination* nextPtr = &next)
{
_next = nextPtr;
Debug.Assert(!Unsafe.IsNullRef(ref next._v1));
_v1.Link(ref next._v1);
}
else
{
Debug.Assert(!Unsafe.IsNullRef(ref next._v2));
_v2.Link(ref next._v2);
}
}
@ -184,10 +295,74 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
public void Unlink()
{
unsafe
if (Unsafe.IsNullRef(ref _v2))
{
_next = null;
_v1.Unlink();
}
else
{
_v2.Unlink();
}
}
/// <summary>
/// Checks if any biquad filter is enabled.
/// </summary>
/// <returns>True if any biquad filter is enabled.</returns>
public bool IsBiquadFilterEnabled()
{
return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabled();
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <returns>True if any biquad filter was previously enabled.</returns>
public bool IsBiquadFilterEnabledPrev()
{
return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabledPrev();
}
/// <summary>
/// Gets the biquad filter parameters.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
/// <returns>Biquad filter parameters.</returns>
public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
{
Debug.Assert(!Unsafe.IsNullRef(ref _v2));
return ref _v2.GetBiquadFilterParameter(index);
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
public void UpdateBiquadFilterEnabledPrev(int index)
{
if (!Unsafe.IsNullRef(ref _v2))
{
_v2.UpdateBiquadFilterEnabledPrev(index);
}
}
/// <summary>
/// Get the reference for the version 1 splitter destination data, or null if version 2 is being used or the destination is null.
/// </summary>
/// <returns>Reference for the version 1 splitter destination data.</returns>
public ref SplitterDestinationVersion1 GetV1RefOrNull()
{
return ref _v1;
}
/// <summary>
/// Get the reference for the version 2 splitter destination data, or null if version 1 is being used or the destination is null.
/// </summary>
/// <returns>Reference for the version 2 splitter destination data.</returns>
public ref SplitterDestinationVersion2 GetV2RefOrNull()
{
return ref _v2;
}
}
}

View file

@ -0,0 +1,206 @@
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Utilities;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
/// <summary>
/// Server state for a splitter destination (version 1).
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
public struct SplitterDestinationVersion1
{
public const int Alignment = 0x10;
/// <summary>
/// The unique id of this <see cref="SplitterDestinationVersion1"/>.
/// </summary>
public int Id;
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
public int DestinationId;
/// <summary>
/// Mix buffer volumes storage.
/// </summary>
private MixArray _mix;
private MixArray _previousMix;
/// <summary>
/// Pointer to the next linked element.
/// </summary>
private unsafe SplitterDestinationVersion1* _next;
/// <summary>
/// Set to true if in use.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
/// <summary>
/// Set to true if the internal state need to be updated.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool NeedToUpdateInternalState;
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
/// <summary>
/// Previous mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
/// <summary>
/// Get the reference of the next element or null if not present.
/// </summary>
public readonly ref SplitterDestinationVersion1 Next
{
get
{
unsafe
{
return ref Unsafe.AsRef<SplitterDestinationVersion1>(_next);
}
}
}
/// <summary>
/// Create a new <see cref="SplitterDestinationVersion1"/>.
/// </summary>
/// <param name="id">The unique id of this <see cref="SplitterDestinationVersion1"/>.</param>
public SplitterDestinationVersion1(int id) : this()
{
Id = id;
DestinationId = Constants.UnusedMixId;
ClearVolumes();
}
/// <summary>
/// Update the <see cref="SplitterDestinationVersion1"/> from user parameter.
/// </summary>
/// <param name="parameter">The user parameter.</param>
public void Update<T>(in T parameter) where T : ISplitterDestinationInParameter
{
Debug.Assert(Id == parameter.Id);
if (parameter.IsMagicValid() && Id == parameter.Id)
{
DestinationId = parameter.DestinationId;
parameter.MixBufferVolume.CopyTo(MixBufferVolume);
if (!IsUsed && parameter.IsUsed)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
NeedToUpdateInternalState = false;
}
IsUsed = parameter.IsUsed;
}
}
/// <summary>
/// Update the internal state of the instance.
/// </summary>
public void UpdateInternalState()
{
if (IsUsed && NeedToUpdateInternalState)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
}
NeedToUpdateInternalState = false;
}
/// <summary>
/// Set the update internal state marker.
/// </summary>
public void MarkAsNeedToUpdateInternalState()
{
NeedToUpdateInternalState = true;
}
/// <summary>
/// Return true if the <see cref="SplitterDestinationVersion1"/> is used and has a destination.
/// </summary>
/// <returns>True if the <see cref="SplitterDestinationVersion1"/> is used and has a destination.</returns>
public readonly bool IsConfigured()
{
return IsUsed && DestinationId != Constants.UnusedMixId;
}
/// <summary>
/// Get the volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolume(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return MixBufferVolume[destinationIndex];
}
/// <summary>
/// Get the previous volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolumePrev(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return PreviousMixBufferVolume[destinationIndex];
}
/// <summary>
/// Clear the volumes.
/// </summary>
public void ClearVolumes()
{
MixBufferVolume.Clear();
PreviousMixBufferVolume.Clear();
}
/// <summary>
/// Link the next element to the given <see cref="SplitterDestinationVersion1"/>.
/// </summary>
/// <param name="next">The given <see cref="SplitterDestinationVersion1"/> to link.</param>
public void Link(ref SplitterDestinationVersion1 next)
{
unsafe
{
fixed (SplitterDestinationVersion1* nextPtr = &next)
{
_next = nextPtr;
}
}
}
/// <summary>
/// Remove the link to the next element.
/// </summary>
public void Unlink()
{
unsafe
{
_next = null;
}
}
}
}

View file

@ -0,0 +1,250 @@
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
/// <summary>
/// Server state for a splitter destination (version 2).
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x110, Pack = Alignment)]
public struct SplitterDestinationVersion2
{
public const int Alignment = 0x10;
/// <summary>
/// The unique id of this <see cref="SplitterDestinationVersion2"/>.
/// </summary>
public int Id;
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
public int DestinationId;
/// <summary>
/// Mix buffer volumes storage.
/// </summary>
private MixArray _mix;
private MixArray _previousMix;
/// <summary>
/// Pointer to the next linked element.
/// </summary>
private unsafe SplitterDestinationVersion2* _next;
/// <summary>
/// Set to true if in use.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
/// <summary>
/// Set to true if the internal state need to be updated.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool NeedToUpdateInternalState;
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
/// <summary>
/// Previous mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
/// <summary>
/// Get the reference of the next element or null if not present.
/// </summary>
public readonly ref SplitterDestinationVersion2 Next
{
get
{
unsafe
{
return ref Unsafe.AsRef<SplitterDestinationVersion2>(_next);
}
}
}
private Array2<BiquadFilterParameter> _biquadFilters;
private Array2<bool> _isPreviousBiquadFilterEnabled;
/// <summary>
/// Create a new <see cref="SplitterDestinationVersion2"/>.
/// </summary>
/// <param name="id">The unique id of this <see cref="SplitterDestinationVersion2"/>.</param>
public SplitterDestinationVersion2(int id) : this()
{
Id = id;
DestinationId = Constants.UnusedMixId;
ClearVolumes();
}
/// <summary>
/// Update the <see cref="SplitterDestinationVersion2"/> from user parameter.
/// </summary>
/// <param name="parameter">The user parameter.</param>
public void Update<T>(in T parameter) where T : ISplitterDestinationInParameter
{
Debug.Assert(Id == parameter.Id);
if (parameter.IsMagicValid() && Id == parameter.Id)
{
DestinationId = parameter.DestinationId;
parameter.MixBufferVolume.CopyTo(MixBufferVolume);
_biquadFilters = parameter.BiquadFilters;
if (!IsUsed && parameter.IsUsed)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
NeedToUpdateInternalState = false;
}
IsUsed = parameter.IsUsed;
}
}
/// <summary>
/// Update the internal state of the instance.
/// </summary>
public void UpdateInternalState()
{
if (IsUsed && NeedToUpdateInternalState)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
}
NeedToUpdateInternalState = false;
}
/// <summary>
/// Set the update internal state marker.
/// </summary>
public void MarkAsNeedToUpdateInternalState()
{
NeedToUpdateInternalState = true;
}
/// <summary>
/// Return true if the <see cref="SplitterDestinationVersion2"/> is used and has a destination.
/// </summary>
/// <returns>True if the <see cref="SplitterDestinationVersion2"/> is used and has a destination.</returns>
public readonly bool IsConfigured()
{
return IsUsed && DestinationId != Constants.UnusedMixId;
}
/// <summary>
/// Get the volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolume(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return MixBufferVolume[destinationIndex];
}
/// <summary>
/// Get the previous volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolumePrev(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return PreviousMixBufferVolume[destinationIndex];
}
/// <summary>
/// Clear the volumes.
/// </summary>
public void ClearVolumes()
{
MixBufferVolume.Clear();
PreviousMixBufferVolume.Clear();
}
/// <summary>
/// Link the next element to the given <see cref="SplitterDestinationVersion2"/>.
/// </summary>
/// <param name="next">The given <see cref="SplitterDestinationVersion2"/> to link.</param>
public void Link(ref SplitterDestinationVersion2 next)
{
unsafe
{
fixed (SplitterDestinationVersion2* nextPtr = &next)
{
_next = nextPtr;
}
}
}
/// <summary>
/// Remove the link to the next element.
/// </summary>
public void Unlink()
{
unsafe
{
_next = null;
}
}
/// <summary>
/// Checks if any biquad filter is enabled.
/// </summary>
/// <returns>True if any biquad filter is enabled.</returns>
public bool IsBiquadFilterEnabled()
{
return _biquadFilters[0].Enable || _biquadFilters[1].Enable;
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <returns>True if any biquad filter was previously enabled.</returns>
public bool IsBiquadFilterEnabledPrev()
{
return _isPreviousBiquadFilterEnabled[0];
}
/// <summary>
/// Gets the biquad filter parameters.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
/// <returns>Biquad filter parameters.</returns>
public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
{
return ref _biquadFilters[index];
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
public void UpdateBiquadFilterEnabledPrev(int index)
{
_isPreviousBiquadFilterEnabled[index] = _biquadFilters[index].Enable;
}
}
}

View file

@ -15,6 +15,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
public const int Alignment = 0x10;
private delegate void SplitterDestinationAction(SplitterDestination destination, int index);
/// <summary>
/// The unique id of this <see cref="SplitterState"/>.
/// </summary>
@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public uint SampleRate;
/// <summary>
/// Count of splitter destinations (<see cref="SplitterDestination"/>).
/// Count of splitter destinations.
/// </summary>
public int DestinationCount;
@ -37,20 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public bool HasNewConnection;
/// <summary>
/// Linked list of <see cref="SplitterDestination"/>.
/// Linked list of <see cref="SplitterDestinationVersion1"/>.
/// </summary>
private unsafe SplitterDestination* _destinationsData;
private unsafe SplitterDestinationVersion1* _destinationDataV1;
/// <summary>
/// Span to the first element of the linked list of <see cref="SplitterDestination"/>.
/// Linked list of <see cref="SplitterDestinationVersion2"/>.
/// </summary>
public readonly Span<SplitterDestination> Destinations
private unsafe SplitterDestinationVersion2* _destinationDataV2;
/// <summary>
/// First element of the linked list of splitter destinations data.
/// </summary>
public readonly SplitterDestination Destination
{
get
{
unsafe
{
return (IntPtr)_destinationsData != IntPtr.Zero ? new Span<SplitterDestination>(_destinationsData, 1) : Span<SplitterDestination>.Empty;
return new SplitterDestination(_destinationDataV1, _destinationDataV2);
}
}
}
@ -64,20 +71,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
Id = id;
}
public readonly Span<SplitterDestination> GetData(int index)
public readonly SplitterDestination GetData(int index)
{
int i = 0;
Span<SplitterDestination> result = Destinations;
SplitterDestination result = Destination;
while (i < index)
{
if (result.IsEmpty)
if (result.IsNull)
{
break;
}
result = result[0].Next;
result = result.Next;
i++;
}
@ -93,25 +100,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
/// <summary>
/// Utility function to apply a given <see cref="SpanAction{T, TArg}"/> to all <see cref="Destinations"/>.
/// Utility function to apply an action to all <see cref="Destination"/>.
/// </summary>
/// <param name="action">The action to execute on each elements.</param>
private readonly void ForEachDestination(SpanAction<SplitterDestination, int> action)
private readonly void ForEachDestination(SplitterDestinationAction action)
{
Span<SplitterDestination> temp = Destinations;
SplitterDestination temp = Destination;
int i = 0;
while (true)
{
if (temp.IsEmpty)
if (temp.IsNull)
{
break;
}
Span<SplitterDestination> next = temp[0].Next;
SplitterDestination next = temp.Next;
action.Invoke(temp, i++);
action(temp, i++);
temp = next;
}
@ -142,9 +149,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
input.ReadLittleEndian(out int destinationId);
Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationId);
SplitterDestination destination = context.GetDestination(destinationId);
SetDestination(ref destination.Span[0]);
SetDestination(destination);
DestinationCount = destinationCount;
@ -152,9 +159,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
input.ReadLittleEndian(out destinationId);
Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationId);
SplitterDestination nextDestination = context.GetDestination(destinationId);
destination.Span[0].Link(ref nextDestination.Span[0]);
destination.Link(nextDestination);
destination = nextDestination;
}
}
@ -174,16 +181,21 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
/// <summary>
/// Set the head of the linked list of <see cref="Destinations"/>.
/// Set the head of the linked list of <see cref="Destination"/>.
/// </summary>
/// <param name="newValue">A reference to a <see cref="SplitterDestination"/>.</param>
public void SetDestination(ref SplitterDestination newValue)
/// <param name="newValue">New destination value.</param>
public void SetDestination(SplitterDestination newValue)
{
unsafe
{
fixed (SplitterDestination* newValuePtr = &newValue)
fixed (SplitterDestinationVersion1* newValuePtr = &newValue.GetV1RefOrNull())
{
_destinationsData = newValuePtr;
_destinationDataV1 = newValuePtr;
}
fixed (SplitterDestinationVersion2* newValuePtr = &newValue.GetV2RefOrNull())
{
_destinationDataV2 = newValuePtr;
}
}
}
@ -193,19 +205,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
public readonly void UpdateInternalState()
{
ForEachDestination((destination, _) => destination[0].UpdateInternalState());
ForEachDestination((destination, _) => destination.UpdateInternalState());
}
/// <summary>
/// Clear all links from the <see cref="Destinations"/>.
/// Clear all links from the <see cref="Destination"/>.
/// </summary>
public void ClearLinks()
{
ForEachDestination((destination, _) => destination[0].Unlink());
ForEachDestination((destination, _) => destination.Unlink());
unsafe
{
_destinationsData = (SplitterDestination*)IntPtr.Zero;
_destinationDataV1 = null;
_destinationDataV2 = null;
}
}
@ -219,7 +232,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
unsafe
{
splitter._destinationsData = (SplitterDestination*)IntPtr.Zero;
splitter._destinationDataV1 = null;
splitter._destinationDataV2 = null;
}
splitter.DestinationCount = 0;

View file

@ -1,7 +1,5 @@
namespace Ryujinx.Common.Configuration.Hid
{
// NOTE: Please don't change this to struct.
// This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys.
public class KeyboardHotkeys
{
public Key ToggleVsync { get; set; }

View file

@ -44,7 +44,7 @@ namespace Ryujinx.Common.Extensions
/// <remarks>
/// DO NOT use <paramref name="copyDestinationIfRequiredDoNotUse"/> after calling this method, as it will only
/// contain a value if the value couldn't be referenced directly because it spans multiple <see cref="ReadOnlyMemory{Byte}"/> segments.
/// To discourage use, it is recommended to to call this method like the following:
/// To discourage use, it is recommended to call this method like the following:
/// <c>
/// ref readonly MyStruct value = ref sequenceReader.GetRefOrRefToCopy{MyStruct}(out _);
/// </c>

View file

@ -0,0 +1,140 @@
#nullable enable
using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace Ryujinx.Common.Memory
{
/// <summary>
/// An <see cref="IMemoryOwner{T}"/> implementation with an embedded length and fast <see cref="Span{T}"/>
/// accessor, with memory allocated from <seealso cref="ArrayPool{T}.Shared"/>.
/// </summary>
/// <typeparam name="T">The type of item to store.</typeparam>
public sealed class MemoryOwner<T> : IMemoryOwner<T>
{
private readonly int _length;
private T[]? _array;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryOwner{T}"/> class with the specified parameters.
/// </summary>
/// <param name="length">The length of the new memory buffer to use</param>
private MemoryOwner(int length)
{
_length = length;
_array = ArrayPool<T>.Shared.Rent(length);
}
/// <summary>
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified length.
/// </summary>
/// <param name="length">The length of the new memory buffer to use</param>
/// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryOwner<T> Rent(int length) => new(length);
/// <summary>
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified length and the content cleared.
/// </summary>
/// <param name="length">The length of the new memory buffer to use</param>
/// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length and the content cleared</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryOwner<T> RentCleared(int length)
{
MemoryOwner<T> result = new(length);
result._array.AsSpan(0, length).Clear();
return result;
}
/// <summary>
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the content copied from the specified buffer.
/// </summary>
/// <param name="buffer">The buffer to copy</param>
/// <returns>A <see cref="MemoryOwner{T}"/> instance with the same length and content as <paramref name="buffer"/></returns>
public static MemoryOwner<T> RentCopy(ReadOnlySpan<T> buffer)
{
MemoryOwner<T> result = new(buffer.Length);
buffer.CopyTo(result._array);
return result;
}
/// <summary>
/// Gets the number of items in the current instance.
/// </summary>
public int Length
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _length;
}
/// <inheritdoc/>
public Memory<T> Memory
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
T[]? array = _array;
if (array is null)
{
ThrowObjectDisposedException();
}
return new(array, 0, _length);
}
}
/// <summary>
/// Gets a <see cref="Span{T}"/> wrapping the memory belonging to the current instance.
/// </summary>
/// <remarks>
/// Uses a trick made possible by the .NET 6+ runtime array layout.
/// </remarks>
public Span<T> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
T[]? array = _array;
if (array is null)
{
ThrowObjectDisposedException();
}
ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(array);
return MemoryMarshal.CreateSpan(ref firstElementRef, _length);
}
}
/// <inheritdoc/>
public void Dispose()
{
T[]? array = Interlocked.Exchange(ref _array, null);
if (array is not null)
{
ArrayPool<T>.Shared.Return(array);
}
}
/// <summary>
/// Throws an <see cref="ObjectDisposedException"/> when <see cref="_array"/> is <see langword="null"/>.
/// </summary>
[DoesNotReturn]
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(MemoryOwner<T>), "The buffer has already been disposed.");
}
}
}

View file

@ -0,0 +1,114 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Common.Memory
{
/// <summary>
/// A stack-only type that rents a buffer of a specified length from <seealso cref="ArrayPool{T}.Shared"/>.
/// It does not implement <see cref="IDisposable"/> to avoid being boxed, but should still be disposed. This
/// is easy since C# 8, which allows use of C# `using` constructs on any type that has a public Dispose() method.
/// To keep this type simple, fast, and read-only, it does not check or guard against multiple disposals.
/// For all these reasons, all usage should be with a `using` block or statement.
/// </summary>
/// <typeparam name="T">The type of item to store.</typeparam>
public readonly ref struct SpanOwner<T>
{
private readonly int _length;
private readonly T[] _array;
/// <summary>
/// Initializes a new instance of the <see cref="SpanOwner{T}"/> struct with the specified parameters.
/// </summary>
/// <param name="length">The length of the new memory buffer to use</param>
private SpanOwner(int length)
{
_length = length;
_array = ArrayPool<T>.Shared.Rent(length);
}
/// <summary>
/// Gets an empty <see cref="SpanOwner{T}"/> instance.
/// </summary>
public static SpanOwner<T> Empty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(0);
}
/// <summary>
/// Creates a new <see cref="SpanOwner{T}"/> instance with the specified length.
/// </summary>
/// <param name="length">The length of the new memory buffer to use</param>
/// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SpanOwner<T> Rent(int length) => new(length);
/// <summary>
/// Creates a new <see cref="SpanOwner{T}"/> instance with the length and the content cleared.
/// </summary>
/// <param name="length">The length of the new memory buffer to use</param>
/// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length and the content cleared</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="length"/> is not valid</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SpanOwner<T> RentCleared(int length)
{
SpanOwner<T> result = new(length);
result._array.AsSpan(0, length).Clear();
return result;
}
/// <summary>
/// Creates a new <see cref="SpanOwner{T}"/> instance with the content copied from the specified buffer.
/// </summary>
/// <param name="buffer">The buffer to copy</param>
/// <returns>A <see cref="SpanOwner{T}"/> instance with the same length and content as <paramref name="buffer"/></returns>
public static SpanOwner<T> RentCopy(ReadOnlySpan<T> buffer)
{
SpanOwner<T> result = new(buffer.Length);
buffer.CopyTo(result._array);
return result;
}
/// <summary>
/// Gets the number of items in the current instance
/// </summary>
public int Length
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _length;
}
/// <summary>
/// Gets a <see cref="Span{T}"/> wrapping the memory belonging to the current instance.
/// </summary>
/// <remarks>
/// Uses a trick made possible by the .NET 6+ runtime array layout.
/// </remarks>
public Span<T> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(_array);
return MemoryMarshal.CreateSpan(ref firstElementRef, _length);
}
}
/// <summary>
/// Implements the duck-typed <see cref="IDisposable.Dispose"/> method.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
ArrayPool<T>.Shared.Return(_array);
}
}
}

View file

@ -85,6 +85,70 @@ namespace Ryujinx.Cpu.Jit
_addressSpace = new(Tracking, backingMemory, _nativePageTable, useProtectionMirrors);
}
public override ReadOnlySequence<byte> GetReadOnlySequence(ulong va, int size, bool tracked = false)
{
if (size == 0)
{
return ReadOnlySequence<byte>.Empty;
}
try
{
if (tracked)
{
SignalMemoryTracking(va, (ulong)size, false);
}
else
{
AssertValidAddressAndSize(va, (ulong)size);
}
ulong endVa = va + (ulong)size;
int offset = 0;
BytesReadOnlySequenceSegment first = null, last = null;
while (va < endVa)
{
(MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(size - offset));
Memory<byte> physicalMemory = memory.GetMemory(rangeOffset, (int)copySize);
if (first is null)
{
first = last = new BytesReadOnlySequenceSegment(physicalMemory);
}
else
{
if (last.IsContiguousWith(physicalMemory, out nuint contiguousStart, out int contiguousSize))
{
Memory<byte> contiguousPhysicalMemory = new NativeMemoryManager<byte>(contiguousStart, contiguousSize).Memory;
last.Replace(contiguousPhysicalMemory);
}
else
{
last = last.Append(physicalMemory);
}
}
va += copySize;
offset += (int)copySize;
}
return new ReadOnlySequence<byte>(first, 0, last, (int)(size - last.RunningIndex));
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
return ReadOnlySequence<byte>.Empty;
}
}
/// <inheritdoc/>
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{

View file

@ -6,8 +6,13 @@ namespace Ryujinx.Graphics.GAL
public enum BufferAccess
{
Default = 0,
FlushPersistent = 1 << 0,
Stream = 1 << 1,
SparseCompatible = 1 << 2,
HostMemory = 1,
DeviceMemory = 2,
DeviceMemoryMapped = 3,
MemoryTypeMask = 0xf,
Stream = 1 << 4,
SparseCompatible = 1 << 5,
}
}

View file

@ -6,6 +6,7 @@ namespace Ryujinx.Graphics.GAL
{
public readonly TargetApi Api;
public readonly string VendorName;
public readonly SystemMemoryType MemoryType;
public readonly bool HasFrontFacingBug;
public readonly bool HasVectorIndexingBug;
@ -36,6 +37,8 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsMismatchingViewFormat;
public readonly bool SupportsCubemapView;
public readonly bool SupportsNonConstantTextureOffset;
public readonly bool SupportsQuads;
public readonly bool SupportsSeparateSampler;
public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence;
public readonly bool SupportsShaderFloat64;
@ -48,6 +51,13 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsIndirectParameters;
public readonly bool SupportsDepthClipControl;
public readonly int UniformBufferSetIndex;
public readonly int StorageBufferSetIndex;
public readonly int TextureSetIndex;
public readonly int ImageSetIndex;
public readonly int ExtraSetBaseIndex;
public readonly int MaximumExtraSets;
public readonly uint MaximumUniformBuffersPerStage;
public readonly uint MaximumStorageBuffersPerStage;
public readonly uint MaximumTexturesPerStage;
@ -64,6 +74,7 @@ namespace Ryujinx.Graphics.GAL
public Capabilities(
TargetApi api,
string vendorName,
SystemMemoryType memoryType,
bool hasFrontFacingBug,
bool hasVectorIndexingBug,
bool needsFragmentOutputSpecialization,
@ -92,6 +103,8 @@ namespace Ryujinx.Graphics.GAL
bool supportsMismatchingViewFormat,
bool supportsCubemapView,
bool supportsNonConstantTextureOffset,
bool supportsQuads,
bool supportsSeparateSampler,
bool supportsShaderBallot,
bool supportsShaderBarrierDivergence,
bool supportsShaderFloat64,
@ -103,6 +116,12 @@ namespace Ryujinx.Graphics.GAL
bool supportsViewportSwizzle,
bool supportsIndirectParameters,
bool supportsDepthClipControl,
int uniformBufferSetIndex,
int storageBufferSetIndex,
int textureSetIndex,
int imageSetIndex,
int extraSetBaseIndex,
int maximumExtraSets,
uint maximumUniformBuffersPerStage,
uint maximumStorageBuffersPerStage,
uint maximumTexturesPerStage,
@ -116,6 +135,7 @@ namespace Ryujinx.Graphics.GAL
{
Api = api;
VendorName = vendorName;
MemoryType = memoryType;
HasFrontFacingBug = hasFrontFacingBug;
HasVectorIndexingBug = hasVectorIndexingBug;
NeedsFragmentOutputSpecialization = needsFragmentOutputSpecialization;
@ -144,6 +164,8 @@ namespace Ryujinx.Graphics.GAL
SupportsMismatchingViewFormat = supportsMismatchingViewFormat;
SupportsCubemapView = supportsCubemapView;
SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset;
SupportsQuads = supportsQuads;
SupportsSeparateSampler = supportsSeparateSampler;
SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
SupportsShaderFloat64 = supportsShaderFloat64;
@ -155,6 +177,12 @@ namespace Ryujinx.Graphics.GAL
SupportsViewportSwizzle = supportsViewportSwizzle;
SupportsIndirectParameters = supportsIndirectParameters;
SupportsDepthClipControl = supportsDepthClipControl;
UniformBufferSetIndex = uniformBufferSetIndex;
StorageBufferSetIndex = storageBufferSetIndex;
TextureSetIndex = textureSetIndex;
ImageSetIndex = imageSetIndex;
ExtraSetBaseIndex = extraSetBaseIndex;
MaximumExtraSets = maximumExtraSets;
MaximumUniformBuffersPerStage = maximumUniformBuffersPerStage;
MaximumStorageBuffersPerStage = maximumStorageBuffersPerStage;
MaximumTexturesPerStage = maximumTexturesPerStage;

View file

@ -711,5 +711,36 @@ namespace Ryujinx.Graphics.GAL
{
return format.IsUint() || format.IsSint();
}
/// <summary>
/// Checks if the texture format is a float or sRGB color format.
/// </summary>
/// <remarks>
/// Does not include normalized, compressed or depth formats.
/// Float and sRGB formats do not participate in logical operations.
/// </remarks>
/// <param name="format">Texture format</param>
/// <returns>True if the format is a float or sRGB color format, false otherwise</returns>
public static bool IsFloatOrSrgb(this Format format)
{
switch (format)
{
case Format.R8G8B8A8Srgb:
case Format.B8G8R8A8Srgb:
case Format.R16Float:
case Format.R16G16Float:
case Format.R16G16B16Float:
case Format.R16G16B16A16Float:
case Format.R32Float:
case Format.R32G32Float:
case Format.R32G32B32Float:
case Format.R32G32B32A32Float:
case Format.R11G11B10Float:
case Format.R9G9B9E5Float:
return true;
}
return false;
}
}
}

View file

@ -1,6 +1,8 @@
using System;
namespace Ryujinx.Graphics.GAL
{
public interface IImageArray
public interface IImageArray : IDisposable
{
void SetFormats(int index, Format[] imageFormats);
void SetImages(int index, ITexture[] images);

View file

@ -60,6 +60,7 @@ namespace Ryujinx.Graphics.GAL
void SetImage(ShaderStage stage, int binding, ITexture texture, Format imageFormat);
void SetImageArray(ShaderStage stage, int binding, IImageArray array);
void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array);
void SetLineParameters(float width, bool smooth);
@ -91,6 +92,7 @@ namespace Ryujinx.Graphics.GAL
void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler);
void SetTextureArray(ShaderStage stage, int binding, ITextureArray array);
void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array);
void SetTransformFeedbackBuffers(ReadOnlySpan<BufferRange> buffers);
void SetUniformBuffers(ReadOnlySpan<BufferAssignment> buffers);

View file

@ -17,7 +17,6 @@ namespace Ryujinx.Graphics.GAL
void BackgroundContextAction(Action action, bool alwaysBackground = false);
BufferHandle CreateBuffer(int size, BufferAccess access = BufferAccess.Default);
BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint);
BufferHandle CreateBuffer(nint pointer, int size);
BufferHandle CreateBufferSparse(ReadOnlySpan<BufferRange> storageBuffers);

View file

@ -1,6 +1,8 @@
using System;
namespace Ryujinx.Graphics.GAL
{
public interface ITextureArray
public interface ITextureArray : IDisposable
{
void SetSamplers(int index, ISampler[] samplers);
void SetTextures(int index, ITexture[] textures);

View file

@ -44,7 +44,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
}
Register<ActionCommand>(CommandType.Action);
Register<CreateBufferCommand>(CommandType.CreateBuffer);
Register<CreateBufferAccessCommand>(CommandType.CreateBufferAccess);
Register<CreateBufferSparseCommand>(CommandType.CreateBufferSparse);
Register<CreateHostBufferCommand>(CommandType.CreateHostBuffer);
@ -67,6 +66,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register<CounterEventDisposeCommand>(CommandType.CounterEventDispose);
Register<CounterEventFlushCommand>(CommandType.CounterEventFlush);
Register<ImageArrayDisposeCommand>(CommandType.ImageArrayDispose);
Register<ImageArraySetFormatsCommand>(CommandType.ImageArraySetFormats);
Register<ImageArraySetImagesCommand>(CommandType.ImageArraySetImages);
@ -89,6 +89,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register<TextureSetDataSliceRegionCommand>(CommandType.TextureSetDataSliceRegion);
Register<TextureSetStorageCommand>(CommandType.TextureSetStorage);
Register<TextureArrayDisposeCommand>(CommandType.TextureArrayDispose);
Register<TextureArraySetSamplersCommand>(CommandType.TextureArraySetSamplers);
Register<TextureArraySetTexturesCommand>(CommandType.TextureArraySetTextures);
@ -125,6 +126,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register<SetUniformBuffersCommand>(CommandType.SetUniformBuffers);
Register<SetImageCommand>(CommandType.SetImage);
Register<SetImageArrayCommand>(CommandType.SetImageArray);
Register<SetImageArraySeparateCommand>(CommandType.SetImageArraySeparate);
Register<SetIndexBufferCommand>(CommandType.SetIndexBuffer);
Register<SetLineParametersCommand>(CommandType.SetLineParameters);
Register<SetLogicOpStateCommand>(CommandType.SetLogicOpState);
@ -142,6 +144,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register<SetStencilTestCommand>(CommandType.SetStencilTest);
Register<SetTextureAndSamplerCommand>(CommandType.SetTextureAndSampler);
Register<SetTextureArrayCommand>(CommandType.SetTextureArray);
Register<SetTextureArraySeparateCommand>(CommandType.SetTextureArraySeparate);
Register<SetUserClipDistanceCommand>(CommandType.SetUserClipDistance);
Register<SetVertexAttribsCommand>(CommandType.SetVertexAttribs);
Register<SetVertexBuffersCommand>(CommandType.SetVertexBuffers);

View file

@ -3,7 +3,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
enum CommandType : byte
{
Action,
CreateBuffer,
CreateBufferAccess,
CreateBufferSparse,
CreateHostBuffer,
@ -27,6 +26,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
CounterEventDispose,
CounterEventFlush,
ImageArrayDispose,
ImageArraySetFormats,
ImageArraySetImages,
@ -49,6 +49,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
TextureSetDataSliceRegion,
TextureSetStorage,
TextureArrayDispose,
TextureArraySetSamplers,
TextureArraySetTextures,
@ -85,6 +86,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
SetUniformBuffers,
SetImage,
SetImageArray,
SetImageArraySeparate,
SetIndexBuffer,
SetLineParameters,
SetLogicOpState,
@ -102,6 +104,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
SetStencilTest,
SetTextureAndSampler,
SetTextureArray,
SetTextureArraySeparate,
SetUserClipDistance,
SetVertexAttribs,
SetVertexBuffers,

View file

@ -0,0 +1,21 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.ImageArray
{
struct ImageArrayDisposeCommand : IGALCommand, IGALCommand<ImageArrayDisposeCommand>
{
public readonly CommandType CommandType => CommandType.ImageArrayDispose;
private TableRef<ThreadedImageArray> _imageArray;
public void Set(TableRef<ThreadedImageArray> imageArray)
{
_imageArray = imageArray;
}
public static void Run(ref ImageArrayDisposeCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
command._imageArray.Get(threaded).Base.Dispose();
}
}
}

View file

@ -1,31 +0,0 @@
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
{
struct CreateBufferCommand : IGALCommand, IGALCommand<CreateBufferCommand>
{
public readonly CommandType CommandType => CommandType.CreateBuffer;
private BufferHandle _threadedHandle;
private int _size;
private BufferAccess _access;
private BufferHandle _storageHint;
public void Set(BufferHandle threadedHandle, int size, BufferAccess access, BufferHandle storageHint)
{
_threadedHandle = threadedHandle;
_size = size;
_access = access;
_storageHint = storageHint;
}
public static void Run(ref CreateBufferCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
BufferHandle hint = BufferHandle.Null;
if (command._storageHint != BufferHandle.Null)
{
hint = threaded.Buffers.MapBuffer(command._storageHint);
}
threaded.Buffers.AssignBuffer(command._threadedHandle, renderer.CreateBuffer(command._size, command._access, hint));
}
}
}

View file

@ -0,0 +1,26 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
struct SetImageArraySeparateCommand : IGALCommand, IGALCommand<SetImageArraySeparateCommand>
{
public readonly CommandType CommandType => CommandType.SetImageArraySeparate;
private ShaderStage _stage;
private int _setIndex;
private TableRef<IImageArray> _array;
public void Set(ShaderStage stage, int setIndex, TableRef<IImageArray> array)
{
_stage = stage;
_setIndex = setIndex;
_array = array;
}
public static void Run(ref SetImageArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
renderer.Pipeline.SetImageArraySeparate(command._stage, command._setIndex, command._array.GetAs<ThreadedImageArray>(threaded)?.Base);
}
}
}

View file

@ -0,0 +1,26 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
struct SetTextureArraySeparateCommand : IGALCommand, IGALCommand<SetTextureArraySeparateCommand>
{
public readonly CommandType CommandType => CommandType.SetTextureArraySeparate;
private ShaderStage _stage;
private int _setIndex;
private TableRef<ITextureArray> _array;
public void Set(ShaderStage stage, int setIndex, TableRef<ITextureArray> array)
{
_stage = stage;
_setIndex = setIndex;
_array = array;
}
public static void Run(ref SetTextureArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
renderer.Pipeline.SetTextureArraySeparate(command._stage, command._setIndex, command._array.GetAs<ThreadedTextureArray>(threaded)?.Base);
}
}
}

View file

@ -0,0 +1,21 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray
{
struct TextureArrayDisposeCommand : IGALCommand, IGALCommand<TextureArrayDisposeCommand>
{
public readonly CommandType CommandType => CommandType.TextureArrayDispose;
private TableRef<ThreadedTextureArray> _textureArray;
public void Set(TableRef<ThreadedTextureArray> textureArray)
{
_textureArray = textureArray;
}
public static void Run(ref TextureArrayDisposeCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
command._textureArray.Get(threaded).Base.Dispose();
}
}
}

View file

@ -21,6 +21,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
return new TableRef<T>(_renderer, reference);
}
public void Dispose()
{
_renderer.New<ImageArrayDisposeCommand>().Set(Ref(this));
_renderer.QueueCommand();
}
public void SetFormats(int index, Format[] imageFormats)
{
_renderer.New<ImageArraySetFormatsCommand>().Set(Ref(this), index, Ref(imageFormats));

View file

@ -22,6 +22,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
return new TableRef<T>(_renderer, reference);
}
public void Dispose()
{
_renderer.New<TextureArrayDisposeCommand>().Set(Ref(this));
_renderer.QueueCommand();
}
public void SetSamplers(int index, ISampler[] samplers)
{
_renderer.New<TextureArraySetSamplersCommand>().Set(Ref(this), index, Ref(samplers.ToArray()));

View file

@ -189,6 +189,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand();
}
public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array)
{
_renderer.New<SetImageArraySeparateCommand>().Set(stage, setIndex, Ref(array));
_renderer.QueueCommand();
}
public void SetIndexBuffer(BufferRange buffer, IndexType type)
{
_renderer.New<SetIndexBufferCommand>().Set(buffer, type);
@ -297,6 +303,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand();
}
public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array)
{
_renderer.New<SetTextureArraySeparateCommand>().Set(stage, setIndex, Ref(array));
_renderer.QueueCommand();
}
public void SetTransformFeedbackBuffers(ReadOnlySpan<BufferRange> buffers)
{
_renderer.New<SetTransformFeedbackBuffersCommand>().Set(_renderer.CopySpan(buffers));

View file

@ -272,15 +272,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return handle;
}
public BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint)
{
BufferHandle handle = Buffers.CreateBufferHandle();
New<CreateBufferCommand>().Set(handle, size, access, storageHint);
QueueCommand();
return handle;
}
public BufferHandle CreateBuffer(nint pointer, int size)
{
BufferHandle handle = Buffers.CreateBufferHandle();

View file

@ -0,0 +1,29 @@
namespace Ryujinx.Graphics.GAL
{
public enum SystemMemoryType
{
/// <summary>
/// The backend manages the ownership of memory. This mode never supports host imported memory.
/// </summary>
BackendManaged,
/// <summary>
/// Device memory has similar performance to host memory, usually because it's shared between CPU/GPU.
/// Use host memory whenever possible.
/// </summary>
UnifiedMemory,
/// <summary>
/// GPU storage to host memory goes though a slow interconnect, but it would still be preferable to use it if the data is flushed back often.
/// Assumes constant buffer access to host memory is rather fast.
/// </summary>
DedicatedMemory,
/// <summary>
/// GPU storage to host memory goes though a slow interconnect, that is very slow when doing access from storage.
/// When frequently accessed, copy buffers to host memory using DMA.
/// Assumes constant buffer access to host memory is rather fast.
/// </summary>
DedicatedMemorySlowStorage
}
}

View file

@ -126,6 +126,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
ulong samplerPoolGpuVa = ((ulong)_state.State.SetTexSamplerPoolAOffsetUpper << 32) | _state.State.SetTexSamplerPoolB;
ulong texturePoolGpuVa = ((ulong)_state.State.SetTexHeaderPoolAOffsetUpper << 32) | _state.State.SetTexHeaderPoolB;
int samplerPoolMaximumId = _state.State.SetTexSamplerPoolCMaximumIndex;
GpuChannelPoolState poolState = new(
texturePoolGpuVa,
_state.State.SetTexHeaderPoolCMaximumIndex,
@ -139,7 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
sharedMemorySize,
_channel.BufferManager.HasUnalignedStorageBuffers);
CachedShaderProgram cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, poolState, computeState, shaderGpuVa);
CachedShaderProgram cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, samplerPoolMaximumId, poolState, computeState, shaderGpuVa);
_context.Renderer.Pipeline.SetProgram(cs.HostProgram);
@ -184,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
sharedMemorySize,
_channel.BufferManager.HasUnalignedStorageBuffers);
cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, poolState, computeState, shaderGpuVa);
cs = memoryManager.Physical.ShaderCache.GetComputeShader(_channel, samplerPoolMaximumId, poolState, computeState, shaderGpuVa);
_context.Renderer.Pipeline.SetProgram(cs.HostProgram);
}

View file

@ -157,6 +157,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
}
else if (operation == SyncpointbOperation.Incr)
{
// "Unbind" render targets since a syncpoint increment might indicate future CPU access for the textures.
_parent.TextureManager.RefreshModifiedTextures();
_context.CreateHostSyncIfNeeded(HostSyncFlags.StrictSyncpoint);
_context.Synchronization.IncrementSyncpoint(syncpointId);
}

View file

@ -4,6 +4,7 @@ using Ryujinx.Graphics.Gpu.Engine.Dma;
using Ryujinx.Graphics.Gpu.Engine.InlineToMemory;
using Ryujinx.Graphics.Gpu.Engine.Threed;
using Ryujinx.Graphics.Gpu.Engine.Twod;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Gpu.Memory;
using System;
using System.Runtime.CompilerServices;
@ -28,6 +29,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
/// </summary>
public MemoryManager MemoryManager => _channel.MemoryManager;
/// <summary>
/// Channel texture manager.
/// </summary>
public TextureManager TextureManager => _channel.TextureManager;
/// <summary>
/// 3D Engine.
/// </summary>

View file

@ -5,6 +5,7 @@ using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.GPFifo;
using Ryujinx.Graphics.Gpu.Engine.Threed;
using Ryujinx.Graphics.Gpu.Engine.Types;
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Memory.Range;
using System;
using System.Collections.Generic;
@ -495,8 +496,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
ulong indirectBufferSize = (ulong)maxDrawCount * (ulong)stride;
MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize);
MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4);
MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize, BufferStage.Indirect);
MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4, BufferStage.Indirect);
_processor.ThreedClass.DrawIndirect(
topology,

View file

@ -438,7 +438,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
ReadOnlySpan<byte> dataBytes = MemoryMarshal.Cast<int, byte>(data);
BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length);
BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length, BufferAccess.DeviceMemory);
_context.Renderer.SetBufferData(buffer, 0, dataBytes);
return new IndexBuffer(buffer, count, dataBytes.Length);
@ -529,7 +529,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
{
if (_dummyBuffer == BufferHandle.Null)
{
_dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize);
_dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize, BufferAccess.DeviceMemory);
_context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0);
}
@ -550,7 +550,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
_context.Renderer.DeleteBuffer(_sequentialIndexBuffer);
}
_sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint));
_sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint), BufferAccess.DeviceMemory);
_sequentialIndexBufferCount = count;
Span<int> data = new int[count];
@ -583,7 +583,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
_context.Renderer.DeleteBuffer(buffer.Handle);
}
buffer.Handle = _context.Renderer.CreateBuffer(newSize);
buffer.Handle = _context.Renderer.CreateBuffer(newSize, BufferAccess.DeviceMemory);
buffer.Size = newSize;
}

View file

@ -3,6 +3,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.Types;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Gpu.Shader;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
@ -370,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
{
var memoryManager = _channel.MemoryManager;
BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size));
BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size), BufferStage.VertexBuffer);
ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format);
bufferTexture.SetStorage(range);
@ -412,7 +413,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
var memoryManager = _channel.MemoryManager;
ulong misalign = address & ((ulong)_context.Capabilities.TextureBufferOffsetAlignment - 1);
BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign));
BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(
memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign),
BufferStage.IndexBuffer);
misalignedOffset = (int)misalign >> shift;
SetIndexBufferTexture(reservations, range, format);

View file

@ -684,8 +684,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (hasCount)
{
var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange);
var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange);
var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect);
var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange, BufferStage.Indirect);
if (indexed)
{
@ -698,7 +698,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else
{
var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange);
var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect);
if (indexed)
{

View file

@ -1429,7 +1429,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
addressesSpan[index] = baseAddress + shader.Offset;
}
CachedShaderProgram gs = shaderCache.GetGraphicsShader(ref _state.State, ref _pipeline, _channel, ref _currentSpecState.GetPoolState(), ref _currentSpecState.GetGraphicsState(), addresses);
int samplerPoolMaximumId = _state.State.SamplerIndex == SamplerIndex.ViaHeaderIndex
? _state.State.TexturePoolState.MaximumId
: _state.State.SamplerPoolState.MaximumId;
CachedShaderProgram gs = shaderCache.GetGraphicsShader(
ref _state.State,
ref _pipeline,
_channel,
samplerPoolMaximumId,
ref _currentSpecState.GetPoolState(),
ref _currentSpecState.GetGraphicsState(),
addresses);
// Consume the modified flag for spec state so that it isn't checked again.
_currentSpecState.SetShader(gs);

View file

@ -393,10 +393,17 @@ namespace Ryujinx.Graphics.Gpu
if (force || _pendingSync || (syncpoint && SyncpointActions.Count > 0))
{
Renderer.CreateSync(SyncNumber, strict);
foreach (var action in SyncActions)
{
action.SyncPreAction(syncpoint);
}
SyncActions.ForEach(action => action.SyncPreAction(syncpoint));
SyncpointActions.ForEach(action => action.SyncPreAction(syncpoint));
foreach (var action in SyncpointActions)
{
action.SyncPreAction(syncpoint);
}
Renderer.CreateSync(SyncNumber, strict);
SyncNumber++;

View file

@ -62,8 +62,9 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="channel">GPU channel that the texture pool cache belongs to</param>
/// <param name="address">Start address of the texture pool</param>
/// <param name="maximumId">Maximum ID of the texture pool</param>
/// <param name="bindingsArrayCache">Cache of texture array bindings</param>
/// <returns>The found or newly created texture pool</returns>
public T FindOrCreate(GpuChannel channel, ulong address, int maximumId)
public T FindOrCreate(GpuChannel channel, ulong address, int maximumId, TextureBindingsArrayCache bindingsArrayCache)
{
// Remove old entries from the cache, if possible.
while (_pools.Count > MaxCapacity && (_currentTimestamp - _pools.First.Value.CacheTimestamp) >= MinDeltaForRemoval)
@ -73,6 +74,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_pools.RemoveFirst();
oldestPool.Dispose();
oldestPool.CacheNode = null;
bindingsArrayCache.RemoveAllWithPool(oldestPool);
}
T pool;
@ -87,8 +89,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (pool.CacheNode != _pools.Last)
{
_pools.Remove(pool.CacheNode);
pool.CacheNode = _pools.AddLast(pool);
_pools.AddLast(pool.CacheNode);
}
pool.CacheTimestamp = _currentTimestamp;

View file

@ -390,7 +390,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
_views.Remove(texture);
Group.RemoveView(texture);
Group.RemoveView(_views, texture);
texture._viewStorage = texture;

View file

@ -19,6 +19,11 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public Format Format { get; }
/// <summary>
/// Shader texture host set index.
/// </summary>
public int Set { get; }
/// <summary>
/// Shader texture host binding point.
/// </summary>
@ -44,20 +49,27 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public TextureUsageFlags Flags { get; }
/// <summary>
/// Indicates that the binding is for a sampler.
/// </summary>
public bool IsSamplerOnly { get; }
/// <summary>
/// Constructs the texture binding information structure.
/// </summary>
/// <param name="target">The shader sampler target type</param>
/// <param name="format">Format of the image as declared on the shader</param>
/// <param name="set">Shader texture host set index</param>
/// <param name="binding">The shader texture binding point</param>
/// <param name="arrayLength">For array of textures, this indicates the length of the array. A value of one indicates it is not an array</param>
/// <param name="cbufSlot">Constant buffer slot where the texture handle is located</param>
/// <param name="handle">The shader texture handle (read index into the texture constant buffer)</param>
/// <param name="flags">The texture's usage flags, indicating how it is used in the shader</param>
public TextureBindingInfo(Target target, Format format, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags)
public TextureBindingInfo(Target target, Format format, int set, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags)
{
Target = target;
Format = format;
Set = set;
Binding = binding;
ArrayLength = arrayLength;
CbufSlot = cbufSlot;
@ -69,13 +81,24 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Constructs the texture binding information structure.
/// </summary>
/// <param name="target">The shader sampler target type</param>
/// <param name="set">Shader texture host set index</param>
/// <param name="binding">The shader texture binding point</param>
/// <param name="arrayLength">For array of textures, this indicates the length of the array. A value of one indicates it is not an array</param>
/// <param name="cbufSlot">Constant buffer slot where the texture handle is located</param>
/// <param name="handle">The shader texture handle (read index into the texture constant buffer)</param>
/// <param name="flags">The texture's usage flags, indicating how it is used in the shader</param>
public TextureBindingInfo(Target target, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags) : this(target, (Format)0, binding, arrayLength, cbufSlot, handle, flags)
/// <param name="isSamplerOnly">Indicates that the binding is for a sampler</param>
public TextureBindingInfo(
Target target,
int set,
int binding,
int arrayLength,
int cbufSlot,
int handle,
TextureUsageFlags flags,
bool isSamplerOnly) : this(target, 0, set, binding, arrayLength, cbufSlot, handle, flags)
{
IsSamplerOnly = isSamplerOnly;
}
}
}

View file

@ -21,12 +21,98 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly GpuContext _context;
private readonly GpuChannel _channel;
private readonly bool _isCompute;
/// <summary>
/// Array cache entry key.
/// </summary>
private readonly struct CacheEntryKey : IEquatable<CacheEntryKey>
private readonly struct CacheEntryFromPoolKey : IEquatable<CacheEntryFromPoolKey>
{
/// <summary>
/// Whether the entry is for an image.
/// </summary>
public readonly bool IsImage;
/// <summary>
/// Whether the entry is for a sampler.
/// </summary>
public readonly bool IsSampler;
/// <summary>
/// Texture or image target type.
/// </summary>
public readonly Target Target;
/// <summary>
/// Number of entries of the array.
/// </summary>
public readonly int ArrayLength;
private readonly TexturePool _texturePool;
private readonly SamplerPool _samplerPool;
/// <summary>
/// Creates a new array cache entry.
/// </summary>
/// <param name="isImage">Whether the entry is for an image</param>
/// <param name="bindingInfo">Binding information for the array</param>
/// <param name="texturePool">Texture pool where the array textures are located</param>
/// <param name="samplerPool">Sampler pool where the array samplers are located</param>
public CacheEntryFromPoolKey(bool isImage, TextureBindingInfo bindingInfo, TexturePool texturePool, SamplerPool samplerPool)
{
IsImage = isImage;
IsSampler = bindingInfo.IsSamplerOnly;
Target = bindingInfo.Target;
ArrayLength = bindingInfo.ArrayLength;
_texturePool = texturePool;
_samplerPool = samplerPool;
}
/// <summary>
/// Checks if the pool matches the cached pool.
/// </summary>
/// <param name="texturePool">Texture or sampler pool instance</param>
/// <returns>True if the pool matches, false otherwise</returns>
public bool MatchesPool<T>(IPool<T> pool)
{
return _texturePool == pool || _samplerPool == pool;
}
/// <summary>
/// Checks if the texture and sampler pools matches the cached pools.
/// </summary>
/// <param name="texturePool">Texture pool instance</param>
/// <param name="samplerPool">Sampler pool instance</param>
/// <returns>True if the pools match, false otherwise</returns>
private bool MatchesPools(TexturePool texturePool, SamplerPool samplerPool)
{
return _texturePool == texturePool && _samplerPool == samplerPool;
}
public bool Equals(CacheEntryFromPoolKey other)
{
return IsImage == other.IsImage &&
IsSampler == other.IsSampler &&
Target == other.Target &&
ArrayLength == other.ArrayLength &&
MatchesPools(other._texturePool, other._samplerPool);
}
public override bool Equals(object obj)
{
return obj is CacheEntryFromBufferKey other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_texturePool, _samplerPool, IsSampler);
}
}
/// <summary>
/// Array cache entry key.
/// </summary>
private readonly struct CacheEntryFromBufferKey : IEquatable<CacheEntryFromBufferKey>
{
/// <summary>
/// Whether the entry is for an image.
@ -61,7 +147,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="texturePool">Texture pool where the array textures are located</param>
/// <param name="samplerPool">Sampler pool where the array samplers are located</param>
/// <param name="textureBufferBounds">Constant buffer bounds with the texture handles</param>
public CacheEntryKey(
public CacheEntryFromBufferKey(
bool isImage,
TextureBindingInfo bindingInfo,
TexturePool texturePool,
@ -100,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Image
return _textureBufferBounds.Equals(textureBufferBounds);
}
public bool Equals(CacheEntryKey other)
public bool Equals(CacheEntryFromBufferKey other)
{
return IsImage == other.IsImage &&
Target == other.Target &&
@ -112,7 +198,7 @@ namespace Ryujinx.Graphics.Gpu.Image
public override bool Equals(object obj)
{
return obj is CacheEntryKey other && Equals(other);
return obj is CacheEntryFromBufferKey other && Equals(other);
}
public override int GetHashCode()
@ -122,40 +208,15 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Array cache entry.
/// Array cache entry from pool.
/// </summary>
private class CacheEntry
{
/// <summary>
/// Key for this entry on the cache.
/// </summary>
public readonly CacheEntryKey Key;
/// <summary>
/// Linked list node used on the texture bindings array cache.
/// </summary>
public LinkedListNode<CacheEntry> CacheNode;
/// <summary>
/// Timestamp set on the last use of the array by the cache.
/// </summary>
public int CacheTimestamp;
/// <summary>
/// All cached textures, along with their invalidated sequence number as value.
/// </summary>
public readonly Dictionary<Texture, int> Textures;
/// <summary>
/// All pool texture IDs along with their textures.
/// </summary>
public readonly Dictionary<int, Texture> TextureIds;
/// <summary>
/// All pool sampler IDs along with their samplers.
/// </summary>
public readonly Dictionary<int, Sampler> SamplerIds;
/// <summary>
/// Backend texture array if the entry is for a texture, otherwise null.
/// </summary>
@ -166,44 +227,39 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public readonly IImageArray ImageArray;
private readonly TexturePool _texturePool;
private readonly SamplerPool _samplerPool;
/// <summary>
/// Texture pool where the array textures are located.
/// </summary>
protected readonly TexturePool TexturePool;
/// <summary>
/// Sampler pool where the array samplers are located.
/// </summary>
protected readonly SamplerPool SamplerPool;
private int _texturePoolSequence;
private int _samplerPoolSequence;
private int[] _cachedTextureBuffer;
private int[] _cachedSamplerBuffer;
private int _lastSequenceNumber;
/// <summary>
/// Creates a new array cache entry.
/// </summary>
/// <param name="key">Key for this entry on the cache</param>
/// <param name="texturePool">Texture pool where the array textures are located</param>
/// <param name="samplerPool">Sampler pool where the array samplers are located</param>
private CacheEntry(ref CacheEntryKey key, TexturePool texturePool, SamplerPool samplerPool)
private CacheEntry(TexturePool texturePool, SamplerPool samplerPool)
{
Key = key;
Textures = new Dictionary<Texture, int>();
TextureIds = new Dictionary<int, Texture>();
SamplerIds = new Dictionary<int, Sampler>();
_texturePool = texturePool;
_samplerPool = samplerPool;
_lastSequenceNumber = -1;
TexturePool = texturePool;
SamplerPool = samplerPool;
}
/// <summary>
/// Creates a new array cache entry.
/// </summary>
/// <param name="key">Key for this entry on the cache</param>
/// <param name="array">Backend texture array</param>
/// <param name="texturePool">Texture pool where the array textures are located</param>
/// <param name="samplerPool">Sampler pool where the array samplers are located</param>
public CacheEntry(ref CacheEntryKey key, ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : this(ref key, texturePool, samplerPool)
public CacheEntry(ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : this(texturePool, samplerPool)
{
TextureArray = array;
}
@ -211,11 +267,10 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary>
/// Creates a new array cache entry.
/// </summary>
/// <param name="key">Key for this entry on the cache</param>
/// <param name="array">Backend image array</param>
/// <param name="texturePool">Texture pool where the array textures are located</param>
/// <param name="samplerPool">Sampler pool where the array samplers are located</param>
public CacheEntry(ref CacheEntryKey key, IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : this(ref key, texturePool, samplerPool)
public CacheEntry(IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : this(texturePool, samplerPool)
{
ImageArray = array;
}
@ -248,23 +303,9 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary>
/// Clears all cached texture instances.
/// </summary>
public void Reset()
public virtual void Reset()
{
Textures.Clear();
TextureIds.Clear();
SamplerIds.Clear();
}
/// <summary>
/// Updates the cached constant buffer data.
/// </summary>
/// <param name="cachedTextureBuffer">Constant buffer data with the texture handles (and sampler handles, if they are combined)</param>
/// <param name="cachedSamplerBuffer">Constant buffer data with the sampler handles</param>
/// <param name="separateSamplerBuffer">Whether <paramref name="cachedTextureBuffer"/> and <paramref name="cachedSamplerBuffer"/> comes from different buffers</param>
public void UpdateData(ReadOnlySpan<int> cachedTextureBuffer, ReadOnlySpan<int> cachedSamplerBuffer, bool separateSamplerBuffer)
{
_cachedTextureBuffer = cachedTextureBuffer.ToArray();
_cachedSamplerBuffer = separateSamplerBuffer ? cachedSamplerBuffer.ToArray() : _cachedTextureBuffer;
}
/// <summary>
@ -287,39 +328,105 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary>
/// Checks if the cached texture or sampler pool has been modified since the last call to this method.
/// </summary>
/// <returns>True if any used entries of the pools might have been modified, false otherwise</returns>
public bool PoolsModified()
/// <returns>True if any used entries of the pool might have been modified, false otherwise</returns>
public bool TexturePoolModified()
{
bool texturePoolModified = _texturePool.WasModified(ref _texturePoolSequence);
bool samplerPoolModified = _samplerPool.WasModified(ref _samplerPoolSequence);
// If both pools were not modified since the last check, we have nothing else to check.
if (!texturePoolModified && !samplerPoolModified)
{
return false;
return TexturePool.WasModified(ref _texturePoolSequence);
}
// If the pools were modified, let's check if any of the entries we care about changed.
// Check if any of our cached textures changed on the pool.
foreach ((int textureId, Texture texture) in TextureIds)
/// <summary>
/// Checks if the cached texture or sampler pool has been modified since the last call to this method.
/// </summary>
/// <returns>True if any used entries of the pool might have been modified, false otherwise</returns>
public bool SamplerPoolModified()
{
if (_texturePool.GetCachedItem(textureId) != texture)
{
return true;
return SamplerPool.WasModified(ref _samplerPoolSequence);
}
}
// Check if any of our cached samplers changed on the pool.
foreach ((int samplerId, Sampler sampler) in SamplerIds)
/// <summary>
/// Array cache entry from constant buffer.
/// </summary>
private class CacheEntryFromBuffer : CacheEntry
{
if (_samplerPool.GetCachedItem(samplerId) != sampler)
/// <summary>
/// Key for this entry on the cache.
/// </summary>
public readonly CacheEntryFromBufferKey Key;
/// <summary>
/// Linked list node used on the texture bindings array cache.
/// </summary>
public LinkedListNode<CacheEntryFromBuffer> CacheNode;
/// <summary>
/// Timestamp set on the last use of the array by the cache.
/// </summary>
public int CacheTimestamp;
/// <summary>
/// All pool texture IDs along with their textures.
/// </summary>
public readonly Dictionary<int, (Texture, TextureDescriptor)> TextureIds;
/// <summary>
/// All pool sampler IDs along with their samplers.
/// </summary>
public readonly Dictionary<int, (Sampler, SamplerDescriptor)> SamplerIds;
private int[] _cachedTextureBuffer;
private int[] _cachedSamplerBuffer;
private int _lastSequenceNumber;
/// <summary>
/// Creates a new array cache entry.
/// </summary>
/// <param name="key">Key for this entry on the cache</param>
/// <param name="array">Backend texture array</param>
/// <param name="texturePool">Texture pool where the array textures are located</param>
/// <param name="samplerPool">Sampler pool where the array samplers are located</param>
public CacheEntryFromBuffer(ref CacheEntryFromBufferKey key, ITextureArray array, TexturePool texturePool, SamplerPool samplerPool) : base(array, texturePool, samplerPool)
{
return true;
}
Key = key;
_lastSequenceNumber = -1;
TextureIds = new Dictionary<int, (Texture, TextureDescriptor)>();
SamplerIds = new Dictionary<int, (Sampler, SamplerDescriptor)>();
}
return false;
/// <summary>
/// Creates a new array cache entry.
/// </summary>
/// <param name="key">Key for this entry on the cache</param>
/// <param name="array">Backend image array</param>
/// <param name="texturePool">Texture pool where the array textures are located</param>
/// <param name="samplerPool">Sampler pool where the array samplers are located</param>
public CacheEntryFromBuffer(ref CacheEntryFromBufferKey key, IImageArray array, TexturePool texturePool, SamplerPool samplerPool) : base(array, texturePool, samplerPool)
{
Key = key;
_lastSequenceNumber = -1;
TextureIds = new Dictionary<int, (Texture, TextureDescriptor)>();
SamplerIds = new Dictionary<int, (Sampler, SamplerDescriptor)>();
}
/// <inheritdoc/>
public override void Reset()
{
base.Reset();
TextureIds.Clear();
SamplerIds.Clear();
}
/// <summary>
/// Updates the cached constant buffer data.
/// </summary>
/// <param name="cachedTextureBuffer">Constant buffer data with the texture handles (and sampler handles, if they are combined)</param>
/// <param name="cachedSamplerBuffer">Constant buffer data with the sampler handles</param>
/// <param name="separateSamplerBuffer">Whether <paramref name="cachedTextureBuffer"/> and <paramref name="cachedSamplerBuffer"/> comes from different buffers</param>
public void UpdateData(ReadOnlySpan<int> cachedTextureBuffer, ReadOnlySpan<int> cachedSamplerBuffer, bool separateSamplerBuffer)
{
_cachedTextureBuffer = cachedTextureBuffer.ToArray();
_cachedSamplerBuffer = separateSamplerBuffer ? cachedSamplerBuffer.ToArray() : _cachedTextureBuffer;
}
/// <summary>
@ -380,10 +487,51 @@ namespace Ryujinx.Graphics.Gpu.Image
return true;
}
/// <summary>
/// Checks if the cached texture or sampler pool has been modified since the last call to this method.
/// </summary>
/// <returns>True if any used entries of the pools might have been modified, false otherwise</returns>
public bool PoolsModified()
{
bool texturePoolModified = TexturePoolModified();
bool samplerPoolModified = SamplerPoolModified();
// If both pools were not modified since the last check, we have nothing else to check.
if (!texturePoolModified && !samplerPoolModified)
{
return false;
}
private readonly Dictionary<CacheEntryKey, CacheEntry> _cache;
private readonly LinkedList<CacheEntry> _lruCache;
// If the pools were modified, let's check if any of the entries we care about changed.
// Check if any of our cached textures changed on the pool.
foreach ((int textureId, (Texture texture, TextureDescriptor descriptor)) in TextureIds)
{
if (TexturePool.GetCachedItem(textureId) != texture ||
(texture == null && TexturePool.IsValidId(textureId) && !TexturePool.GetDescriptorRef(textureId).Equals(descriptor)))
{
return true;
}
}
// Check if any of our cached samplers changed on the pool.
foreach ((int samplerId, (Sampler sampler, SamplerDescriptor descriptor)) in SamplerIds)
{
if (SamplerPool.GetCachedItem(samplerId) != sampler ||
(sampler == null && SamplerPool.IsValidId(samplerId) && !SamplerPool.GetDescriptorRef(samplerId).Equals(descriptor)))
{
return true;
}
}
return false;
}
}
private readonly Dictionary<CacheEntryFromBufferKey, CacheEntryFromBuffer> _cacheFromBuffer;
private readonly Dictionary<CacheEntryFromPoolKey, CacheEntry> _cacheFromPool;
private readonly LinkedList<CacheEntryFromBuffer> _lruCache;
private int _currentTimestamp;
@ -392,14 +540,13 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="channel">GPU channel</param>
/// <param name="isCompute">Whether the bindings will be used for compute or graphics pipelines</param>
public TextureBindingsArrayCache(GpuContext context, GpuChannel channel, bool isCompute)
public TextureBindingsArrayCache(GpuContext context, GpuChannel channel)
{
_context = context;
_channel = channel;
_isCompute = isCompute;
_cache = new Dictionary<CacheEntryKey, CacheEntry>();
_lruCache = new LinkedList<CacheEntry>();
_cacheFromBuffer = new Dictionary<CacheEntryFromBufferKey, CacheEntryFromBuffer>();
_cacheFromPool = new Dictionary<CacheEntryFromPoolKey, CacheEntry>();
_lruCache = new LinkedList<CacheEntryFromBuffer>();
}
/// <summary>
@ -419,7 +566,7 @@ namespace Ryujinx.Graphics.Gpu.Image
int stageIndex,
int textureBufferIndex,
SamplerIndex samplerIndex,
TextureBindingInfo bindingInfo)
in TextureBindingInfo bindingInfo)
{
Update(texturePool, samplerPool, stage, stageIndex, textureBufferIndex, isImage: false, samplerIndex, bindingInfo);
}
@ -432,7 +579,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="stageIndex">Shader stage index where the array is used</param>
/// <param name="textureBufferIndex">Texture constant buffer index</param>
/// <param name="bindingInfo">Array binding information</param>
public void UpdateImageArray(TexturePool texturePool, ShaderStage stage, int stageIndex, int textureBufferIndex, TextureBindingInfo bindingInfo)
public void UpdateImageArray(TexturePool texturePool, ShaderStage stage, int stageIndex, int textureBufferIndex, in TextureBindingInfo bindingInfo)
{
Update(texturePool, null, stage, stageIndex, textureBufferIndex, isImage: true, SamplerIndex.ViaHeaderIndex, bindingInfo);
}
@ -456,16 +603,181 @@ namespace Ryujinx.Graphics.Gpu.Image
int textureBufferIndex,
bool isImage,
SamplerIndex samplerIndex,
TextureBindingInfo bindingInfo)
in TextureBindingInfo bindingInfo)
{
if (IsDirectHandleType(bindingInfo.Handle))
{
UpdateFromPool(texturePool, samplerPool, stage, isImage, bindingInfo);
}
else
{
UpdateFromBuffer(texturePool, samplerPool, stage, stageIndex, textureBufferIndex, isImage, samplerIndex, bindingInfo);
}
}
/// <summary>
/// Updates a texture or image array bindings and textures from a texture or sampler pool.
/// </summary>
/// <param name="texturePool">Texture pool</param>
/// <param name="samplerPool">Sampler pool</param>
/// <param name="stage">Shader stage where the array is used</param>
/// <param name="isImage">Whether the array is a image or texture array</param>
/// <param name="bindingInfo">Array binding information</param>
private void UpdateFromPool(TexturePool texturePool, SamplerPool samplerPool, ShaderStage stage, bool isImage, in TextureBindingInfo bindingInfo)
{
CacheEntry entry = GetOrAddEntry(texturePool, samplerPool, bindingInfo, isImage, out bool isNewEntry);
bool isSampler = bindingInfo.IsSamplerOnly;
bool poolModified = isSampler ? entry.SamplerPoolModified() : entry.TexturePoolModified();
bool isStore = bindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore);
bool resScaleUnsupported = bindingInfo.Flags.HasFlag(TextureUsageFlags.ResScaleUnsupported);
if (!poolModified && !isNewEntry && entry.ValidateTextures())
{
entry.SynchronizeMemory(isStore, resScaleUnsupported);
if (isImage)
{
SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
return;
}
if (!isNewEntry)
{
entry.Reset();
}
int length = (isSampler ? samplerPool.MaximumId : texturePool.MaximumId) + 1;
length = Math.Min(length, bindingInfo.ArrayLength);
Format[] formats = isImage ? new Format[bindingInfo.ArrayLength] : null;
ISampler[] samplers = isImage ? null : new ISampler[bindingInfo.ArrayLength];
ITexture[] textures = new ITexture[bindingInfo.ArrayLength];
for (int index = 0; index < length; index++)
{
Texture texture = null;
Sampler sampler = null;
if (isSampler)
{
sampler = samplerPool?.Get(index);
}
else
{
ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(index, out texture);
if (texture != null)
{
entry.Textures[texture] = texture.InvalidatedSequence;
if (isStore)
{
texture.SignalModified();
}
if (resScaleUnsupported && texture.ScaleMode != TextureScaleMode.Blacklisted)
{
// Scaling textures used on arrays is currently not supported.
texture.BlacklistScale();
}
}
}
ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target);
ISampler hostSampler = sampler?.GetHostSampler(texture);
Format format = bindingInfo.Format;
if (hostTexture != null && texture.Target == Target.TextureBuffer)
{
// Ensure that the buffer texture is using the correct buffer as storage.
// Buffers are frequently re-created to accommodate larger data, so we need to re-bind
// to ensure we're not using a old buffer that was already deleted.
if (isImage)
{
if (format == 0 && texture != null)
{
format = texture.Format;
}
_channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format);
}
else
{
_channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format);
}
}
else if (isImage)
{
if (format == 0 && texture != null)
{
format = texture.Format;
}
formats[index] = format;
textures[index] = hostTexture;
}
else
{
samplers[index] = hostSampler;
textures[index] = hostTexture;
}
}
if (isImage)
{
entry.ImageArray.SetFormats(0, formats);
entry.ImageArray.SetImages(0, textures);
SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
entry.TextureArray.SetSamplers(0, samplers);
entry.TextureArray.SetTextures(0, textures);
SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
}
/// <summary>
/// Updates a texture or image array bindings and textures from constant buffer handles.
/// </summary>
/// <param name="texturePool">Texture pool</param>
/// <param name="samplerPool">Sampler pool</param>
/// <param name="stage">Shader stage where the array is used</param>
/// <param name="stageIndex">Shader stage index where the array is used</param>
/// <param name="textureBufferIndex">Texture constant buffer index</param>
/// <param name="isImage">Whether the array is a image or texture array</param>
/// <param name="samplerIndex">Sampler handles source</param>
/// <param name="bindingInfo">Array binding information</param>
private void UpdateFromBuffer(
TexturePool texturePool,
SamplerPool samplerPool,
ShaderStage stage,
int stageIndex,
int textureBufferIndex,
bool isImage,
SamplerIndex samplerIndex,
in TextureBindingInfo bindingInfo)
{
(textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, textureBufferIndex);
bool separateSamplerBuffer = textureBufferIndex != samplerBufferIndex;
bool isCompute = stage == ShaderStage.Compute;
ref BufferBounds textureBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(_isCompute, stageIndex, textureBufferIndex);
ref BufferBounds samplerBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(_isCompute, stageIndex, samplerBufferIndex);
ref BufferBounds textureBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(isCompute, stageIndex, textureBufferIndex);
ref BufferBounds samplerBufferBounds = ref _channel.BufferManager.GetUniformBufferBounds(isCompute, stageIndex, samplerBufferIndex);
CacheEntry entry = GetOrAddEntry(
CacheEntryFromBuffer entry = GetOrAddEntry(
texturePool,
samplerPool,
bindingInfo,
@ -488,11 +800,11 @@ namespace Ryujinx.Graphics.Gpu.Image
if (isImage)
{
_context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
_context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
return;
@ -517,11 +829,11 @@ namespace Ryujinx.Graphics.Gpu.Image
if (isImage)
{
_context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
_context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
return;
@ -589,8 +901,8 @@ namespace Ryujinx.Graphics.Gpu.Image
Sampler sampler = samplerPool?.Get(samplerId);
entry.TextureIds[textureId] = texture;
entry.SamplerIds[samplerId] = sampler;
entry.TextureIds[textureId] = (texture, descriptor);
entry.SamplerIds[samplerId] = (sampler, samplerPool?.GetDescriptorRef(samplerId) ?? default);
ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target);
ISampler hostSampler = sampler?.GetHostSampler(texture);
@ -609,11 +921,11 @@ namespace Ryujinx.Graphics.Gpu.Image
format = texture.Format;
}
_channel.BufferManager.SetBufferTextureStorage(entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format);
_channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format);
}
else
{
_channel.BufferManager.SetBufferTextureStorage(entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format);
_channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format);
}
}
else if (isImage)
@ -638,43 +950,72 @@ namespace Ryujinx.Graphics.Gpu.Image
entry.ImageArray.SetFormats(0, formats);
entry.ImageArray.SetImages(0, textures);
_context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
entry.TextureArray.SetSamplers(0, samplers);
entry.TextureArray.SetTextures(0, textures);
_context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
}
/// <summary>
/// Gets a cached texture entry, or creates a new one if not found.
/// Updates a texture array binding on the host.
/// </summary>
/// <param name="stage">Shader stage where the array is used</param>
/// <param name="bindingInfo">Array binding information</param>
/// <param name="array">Texture array</param>
private void SetTextureArray(ShaderStage stage, in TextureBindingInfo bindingInfo, ITextureArray array)
{
if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0)
{
_context.Renderer.Pipeline.SetTextureArraySeparate(stage, bindingInfo.Set, array);
}
else
{
_context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, array);
}
}
/// <summary>
/// Updates a image array binding on the host.
/// </summary>
/// <param name="stage">Shader stage where the array is used</param>
/// <param name="bindingInfo">Array binding information</param>
/// <param name="array">Image array</param>
private void SetImageArray(ShaderStage stage, in TextureBindingInfo bindingInfo, IImageArray array)
{
if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0)
{
_context.Renderer.Pipeline.SetImageArraySeparate(stage, bindingInfo.Set, array);
}
else
{
_context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, array);
}
}
/// <summary>
/// Gets a cached texture entry from pool, or creates a new one if not found.
/// </summary>
/// <param name="texturePool">Texture pool</param>
/// <param name="samplerPool">Sampler pool</param>
/// <param name="bindingInfo">Array binding information</param>
/// <param name="isImage">Whether the array is a image or texture array</param>
/// <param name="textureBufferBounds">Constant buffer bounds with the texture handles</param>
/// <param name="isNew">Whether a new entry was created, or an existing one was returned</param>
/// <returns>Cache entry</returns>
private CacheEntry GetOrAddEntry(
TexturePool texturePool,
SamplerPool samplerPool,
TextureBindingInfo bindingInfo,
in TextureBindingInfo bindingInfo,
bool isImage,
ref BufferBounds textureBufferBounds,
out bool isNew)
{
CacheEntryKey key = new CacheEntryKey(
isImage,
bindingInfo,
texturePool,
samplerPool,
ref textureBufferBounds);
CacheEntryFromPoolKey key = new CacheEntryFromPoolKey(isImage, bindingInfo, texturePool, samplerPool);
isNew = !_cache.TryGetValue(key, out CacheEntry entry);
isNew = !_cacheFromPool.TryGetValue(key, out CacheEntry entry);
if (isNew)
{
@ -684,13 +1025,61 @@ namespace Ryujinx.Graphics.Gpu.Image
{
IImageArray array = _context.Renderer.CreateImageArray(arrayLength, bindingInfo.Target == Target.TextureBuffer);
_cache.Add(key, entry = new CacheEntry(ref key, array, texturePool, samplerPool));
_cacheFromPool.Add(key, entry = new CacheEntry(array, texturePool, samplerPool));
}
else
{
ITextureArray array = _context.Renderer.CreateTextureArray(arrayLength, bindingInfo.Target == Target.TextureBuffer);
_cache.Add(key, entry = new CacheEntry(ref key, array, texturePool, samplerPool));
_cacheFromPool.Add(key, entry = new CacheEntry(array, texturePool, samplerPool));
}
}
return entry;
}
/// <summary>
/// Gets a cached texture entry from constant buffer, or creates a new one if not found.
/// </summary>
/// <param name="texturePool">Texture pool</param>
/// <param name="samplerPool">Sampler pool</param>
/// <param name="bindingInfo">Array binding information</param>
/// <param name="isImage">Whether the array is a image or texture array</param>
/// <param name="textureBufferBounds">Constant buffer bounds with the texture handles</param>
/// <param name="isNew">Whether a new entry was created, or an existing one was returned</param>
/// <returns>Cache entry</returns>
private CacheEntryFromBuffer GetOrAddEntry(
TexturePool texturePool,
SamplerPool samplerPool,
in TextureBindingInfo bindingInfo,
bool isImage,
ref BufferBounds textureBufferBounds,
out bool isNew)
{
CacheEntryFromBufferKey key = new CacheEntryFromBufferKey(
isImage,
bindingInfo,
texturePool,
samplerPool,
ref textureBufferBounds);
isNew = !_cacheFromBuffer.TryGetValue(key, out CacheEntryFromBuffer entry);
if (isNew)
{
int arrayLength = bindingInfo.ArrayLength;
if (isImage)
{
IImageArray array = _context.Renderer.CreateImageArray(arrayLength, bindingInfo.Target == Target.TextureBuffer);
_cacheFromBuffer.Add(key, entry = new CacheEntryFromBuffer(ref key, array, texturePool, samplerPool));
}
else
{
ITextureArray array = _context.Renderer.CreateTextureArray(arrayLength, bindingInfo.Target == Target.TextureBuffer);
_cacheFromBuffer.Add(key, entry = new CacheEntryFromBuffer(ref key, array, texturePool, samplerPool));
}
}
@ -716,15 +1105,70 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
private void RemoveLeastUsedEntries()
{
LinkedListNode<CacheEntry> nextNode = _lruCache.First;
LinkedListNode<CacheEntryFromBuffer> nextNode = _lruCache.First;
while (nextNode != null && _currentTimestamp - nextNode.Value.CacheTimestamp >= MinDeltaForRemoval)
{
LinkedListNode<CacheEntry> toRemove = nextNode;
LinkedListNode<CacheEntryFromBuffer> toRemove = nextNode;
nextNode = nextNode.Next;
_cache.Remove(toRemove.Value.Key);
_cacheFromBuffer.Remove(toRemove.Value.Key);
_lruCache.Remove(toRemove);
if (toRemove.Value.Key.IsImage)
{
toRemove.Value.ImageArray.Dispose();
}
else
{
toRemove.Value.TextureArray.Dispose();
}
}
}
/// <summary>
/// Removes all cached texture arrays matching the specified texture pool.
/// </summary>
/// <param name="pool">Texture pool</param>
public void RemoveAllWithPool<T>(IPool<T> pool)
{
List<CacheEntryFromPoolKey> keysToRemove = null;
foreach ((CacheEntryFromPoolKey key, CacheEntry entry) in _cacheFromPool)
{
if (key.MatchesPool(pool))
{
(keysToRemove ??= new()).Add(key);
if (key.IsImage)
{
entry.ImageArray.Dispose();
}
else
{
entry.TextureArray.Dispose();
}
}
}
if (keysToRemove != null)
{
foreach (CacheEntryFromPoolKey key in keysToRemove)
{
_cacheFromPool.Remove(key);
}
}
}
/// <summary>
/// Checks if a handle indicates the binding should have all its textures sourced directly from a pool.
/// </summary>
/// <param name="handle">Handle to check</param>
/// <returns>True if the handle represents direct pool access, false otherwise</returns>
private static bool IsDirectHandleType(int handle)
{
(_, _, TextureHandleType type) = TextureHandle.UnpackOffsets(handle);
return type == TextureHandleType.Direct;
}
}
}

View file

@ -34,7 +34,7 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly TexturePoolCache _texturePoolCache;
private readonly SamplerPoolCache _samplerPoolCache;
private readonly TextureBindingsArrayCache _arrayBindingsCache;
private readonly TextureBindingsArrayCache _bindingsArrayCache;
private TexturePool _cachedTexturePool;
private SamplerPool _cachedSamplerPool;
@ -72,12 +72,14 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
/// <param name="context">The GPU context that the texture bindings manager belongs to</param>
/// <param name="channel">The GPU channel that the texture bindings manager belongs to</param>
/// <param name="bindingsArrayCache">Cache of texture array bindings</param>
/// <param name="texturePoolCache">Texture pools cache used to get texture pools from</param>
/// <param name="samplerPoolCache">Sampler pools cache used to get sampler pools from</param>
/// <param name="isCompute">True if the bindings manager is used for the compute engine</param>
public TextureBindingsManager(
GpuContext context,
GpuChannel channel,
TextureBindingsArrayCache bindingsArrayCache,
TexturePoolCache texturePoolCache,
SamplerPoolCache samplerPoolCache,
bool isCompute)
@ -89,7 +91,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_isCompute = isCompute;
_arrayBindingsCache = new TextureBindingsArrayCache(context, channel, isCompute);
_bindingsArrayCache = bindingsArrayCache;
int stages = isCompute ? 1 : Constants.ShaderStages;
@ -456,7 +458,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (bindingInfo.ArrayLength > 1)
{
_arrayBindingsCache.UpdateTextureArray(texturePool, samplerPool, stage, stageIndex, _textureBufferIndex, _samplerIndex, bindingInfo);
_bindingsArrayCache.UpdateTextureArray(texturePool, samplerPool, stage, stageIndex, _textureBufferIndex, _samplerIndex, bindingInfo);
continue;
}
@ -594,7 +596,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (bindingInfo.ArrayLength > 1)
{
_arrayBindingsCache.UpdateImageArray(pool, stage, stageIndex, _textureBufferIndex, bindingInfo);
_bindingsArrayCache.UpdateImageArray(pool, stage, stageIndex, _textureBufferIndex, bindingInfo);
continue;
}
@ -732,7 +734,7 @@ namespace Ryujinx.Graphics.Gpu.Image
ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa);
TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId);
TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId, _bindingsArrayCache);
TextureDescriptor descriptor;
@ -770,7 +772,7 @@ namespace Ryujinx.Graphics.Gpu.Image
? _channel.BufferManager.GetComputeUniformBufferAddress(textureBufferIndex)
: _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, textureBufferIndex);
int handle = textureBufferAddress != 0
int handle = textureBufferAddress != MemoryManager.PteUnmapped
? _channel.MemoryManager.Physical.Read<int>(textureBufferAddress + (uint)textureWordOffset * 4)
: 0;
@ -790,7 +792,7 @@ namespace Ryujinx.Graphics.Gpu.Image
? _channel.BufferManager.GetComputeUniformBufferAddress(samplerBufferIndex)
: _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, samplerBufferIndex);
samplerHandle = samplerBufferAddress != 0
samplerHandle = samplerBufferAddress != MemoryManager.PteUnmapped
? _channel.MemoryManager.Physical.Read<int>(samplerBufferAddress + (uint)samplerWordOffset * 4)
: 0;
}
@ -828,7 +830,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (poolAddress != MemoryManager.PteUnmapped)
{
texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId);
texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId, _bindingsArrayCache);
_texturePool = texturePool;
}
}
@ -839,7 +841,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (poolAddress != MemoryManager.PteUnmapped)
{
samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId);
samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId, _bindingsArrayCache);
_samplerPool = samplerPool;
}
}

View file

@ -8,6 +8,7 @@ using Ryujinx.Graphics.Texture;
using Ryujinx.Memory.Range;
using System;
using System.Collections.Generic;
using System.Threading;
namespace Ryujinx.Graphics.Gpu.Image
{
@ -39,6 +40,8 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly MultiRangeList<Texture> _textures;
private readonly HashSet<Texture> _partiallyMappedTextures;
private readonly ReaderWriterLockSlim _texturesLock;
private Texture[] _textureOverlaps;
private OverlapInfo[] _overlapInfo;
@ -57,6 +60,8 @@ namespace Ryujinx.Graphics.Gpu.Image
_textures = new MultiRangeList<Texture>();
_partiallyMappedTextures = new HashSet<Texture>();
_texturesLock = new ReaderWriterLockSlim();
_textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
_overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
@ -75,10 +80,16 @@ namespace Ryujinx.Graphics.Gpu.Image
MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
lock (_textures)
_texturesLock.EnterReadLock();
try
{
overlapCount = _textures.FindOverlaps(unmapped, ref overlaps);
}
finally
{
_texturesLock.ExitReadLock();
}
if (overlapCount > 0)
{
@ -217,7 +228,18 @@ namespace Ryujinx.Graphics.Gpu.Image
public bool UpdateMapping(Texture texture, MultiRange range)
{
// There cannot be an existing texture compatible with this mapping in the texture cache already.
int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps);
int overlapCount;
_texturesLock.EnterReadLock();
try
{
overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps);
}
finally
{
_texturesLock.ExitReadLock();
}
for (int i = 0; i < overlapCount; i++)
{
@ -231,11 +253,20 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
_texturesLock.EnterWriteLock();
try
{
_textures.Remove(texture);
texture.ReplaceRange(range);
_textures.Add(texture);
}
finally
{
_texturesLock.ExitWriteLock();
}
return true;
}
@ -611,11 +642,17 @@ namespace Ryujinx.Graphics.Gpu.Image
int sameAddressOverlapsCount;
lock (_textures)
_texturesLock.EnterReadLock();
try
{
// Try to find a perfect texture match, with the same address and parameters.
sameAddressOverlapsCount = _textures.FindOverlaps(address, ref _textureOverlaps);
}
finally
{
_texturesLock.ExitReadLock();
}
Texture texture = null;
@ -698,10 +735,16 @@ namespace Ryujinx.Graphics.Gpu.Image
if (info.Target != Target.TextureBuffer)
{
lock (_textures)
_texturesLock.EnterReadLock();
try
{
overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
}
finally
{
_texturesLock.ExitReadLock();
}
}
if (_overlapInfo.Length != _textureOverlaps.Length)
@ -1025,10 +1068,16 @@ namespace Ryujinx.Graphics.Gpu.Image
_cache.Add(texture);
}
lock (_textures)
_texturesLock.EnterWriteLock();
try
{
_textures.Add(texture);
}
finally
{
_texturesLock.ExitWriteLock();
}
if (partiallyMapped)
{
@ -1091,7 +1140,19 @@ namespace Ryujinx.Graphics.Gpu.Image
return null;
}
int addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps);
int addressMatches;
_texturesLock.EnterReadLock();
try
{
addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps);
}
finally
{
_texturesLock.ExitReadLock();
}
Texture textureMatch = null;
for (int i = 0; i < addressMatches; i++)
@ -1232,10 +1293,16 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="texture">The texture to be removed</param>
public void RemoveTextureFromCache(Texture texture)
{
lock (_textures)
_texturesLock.EnterWriteLock();
try
{
_textures.Remove(texture);
}
finally
{
_texturesLock.ExitWriteLock();
}
lock (_partiallyMappedTextures)
{
@ -1324,13 +1391,19 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public void Dispose()
{
lock (_textures)
_texturesLock.EnterReadLock();
try
{
foreach (Texture texture in _textures)
{
texture.Dispose();
}
}
finally
{
_texturesLock.ExitReadLock();
}
}
}
}

View file

@ -88,9 +88,9 @@ namespace Ryujinx.Graphics.Gpu.Image
private MultiRange TextureRange => Storage.Range;
/// <summary>
/// The views list from the storage texture.
/// The views array from the storage texture.
/// </summary>
private List<Texture> _views;
private Texture[] _views;
private TextureGroupHandle[] _handles;
private bool[] _loadNeeded;
@ -645,7 +645,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
else
{
_flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.FlushPersistent);
_flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.HostMemory);
_flushBufferImported = false;
}
@ -1074,7 +1074,7 @@ namespace Ryujinx.Graphics.Gpu.Image
public void UpdateViews(List<Texture> views, Texture texture)
{
// This is saved to calculate overlapping views for each handle.
_views = views;
_views = views.ToArray();
bool layerViews = _hasLayerViews;
bool mipViews = _hasMipViews;
@ -1136,9 +1136,13 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary>
/// Removes a view from the group, removing it from all overlap lists.
/// </summary>
/// <param name="views">The views list of the storage texture</param>
/// <param name="view">View to remove from the group</param>
public void RemoveView(Texture view)
public void RemoveView(List<Texture> views, Texture view)
{
// This is saved to calculate overlapping views for each handle.
_views = views.ToArray();
int offset = FindOffset(view);
foreach (TextureGroupHandle handle in _handles)
@ -1605,9 +1609,11 @@ namespace Ryujinx.Graphics.Gpu.Image
Storage.SignalModifiedDirty();
if (_views != null)
Texture[] views = _views;
if (views != null)
{
foreach (Texture texture in _views)
foreach (Texture texture in views)
{
texture.SignalModifiedDirty();
}

View file

@ -121,7 +121,7 @@ namespace Ryujinx.Graphics.Gpu.Image
public TextureGroupHandle(TextureGroup group,
int offset,
ulong size,
List<Texture> views,
IEnumerable<Texture> views,
int firstLayer,
int firstLevel,
int baseSlice,
@ -201,8 +201,8 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Calculate a list of which views overlap this handle.
/// </summary>
/// <param name="group">The parent texture group, used to find a view's base CPU VA offset</param>
/// <param name="views">The list of views to search for overlaps</param>
public void RecalculateOverlaps(TextureGroup group, List<Texture> views)
/// <param name="views">The views to search for overlaps</param>
public void RecalculateOverlaps(TextureGroup group, IEnumerable<Texture> views)
{
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
lock (Overlaps)

View file

@ -15,6 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly TextureBindingsManager _cpBindingsManager;
private readonly TextureBindingsManager _gpBindingsManager;
private readonly TextureBindingsArrayCache _bindingsArrayCache;
private readonly TexturePoolCache _texturePoolCache;
private readonly SamplerPoolCache _samplerPoolCache;
@ -46,8 +47,9 @@ namespace Ryujinx.Graphics.Gpu.Image
TexturePoolCache texturePoolCache = new(context);
SamplerPoolCache samplerPoolCache = new(context);
_cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, isCompute: true);
_gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, isCompute: false);
_bindingsArrayCache = new TextureBindingsArrayCache(context, channel);
_cpBindingsManager = new TextureBindingsManager(context, channel, _bindingsArrayCache, texturePoolCache, samplerPoolCache, isCompute: true);
_gpBindingsManager = new TextureBindingsManager(context, channel, _bindingsArrayCache, texturePoolCache, samplerPoolCache, isCompute: false);
_texturePoolCache = texturePoolCache;
_samplerPoolCache = samplerPoolCache;
@ -384,7 +386,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa);
TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId);
TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId, _bindingsArrayCache);
return texturePool;
}

View file

@ -10,6 +10,8 @@ using System.Threading;
namespace Ryujinx.Graphics.Gpu.Memory
{
delegate void BufferFlushAction(ulong address, ulong size, ulong syncNumber);
/// <summary>
/// Buffer, used to store vertex and index data, uniform and storage buffers, and others.
/// </summary>
@ -23,7 +25,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary>
/// Host buffer handle.
/// </summary>
public BufferHandle Handle { get; }
public BufferHandle Handle { get; private set; }
/// <summary>
/// Start address of the buffer in guest memory.
@ -60,6 +62,17 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </remarks>
private BufferModifiedRangeList _modifiedRanges = null;
/// <summary>
/// A structure that is used to flush buffer data back to a host mapped buffer for cached readback.
/// Only used if the buffer data is explicitly owned by device local memory.
/// </summary>
private BufferPreFlush _preFlush = null;
/// <summary>
/// Usage tracking state that determines what type of backing the buffer should use.
/// </summary>
public BufferBackingState BackingState;
private readonly MultiRegionHandle _memoryTrackingGranular;
private readonly RegionHandle _memoryTracking;
@ -87,6 +100,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="physicalMemory">Physical memory where the buffer is mapped</param>
/// <param name="address">Start address of the buffer</param>
/// <param name="size">Size of the buffer in bytes</param>
/// <param name="stage">The type of usage that created the buffer</param>
/// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param>
/// <param name="baseBuffers">Buffers which this buffer contains, and will inherit tracking handles from</param>
public Buffer(
@ -94,6 +108,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
PhysicalMemory physicalMemory,
ulong address,
ulong size,
BufferStage stage,
bool sparseCompatible,
IEnumerable<Buffer> baseBuffers = null)
{
@ -103,9 +118,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
Size = size;
SparseCompatible = sparseCompatible;
BufferAccess access = sparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default;
BackingState = new BufferBackingState(_context, this, stage, baseBuffers);
Handle = context.Renderer.CreateBuffer((int)size, access, baseBuffers?.MaxBy(x => x.Size).Handle ?? BufferHandle.Null);
BufferAccess access = BackingState.SwitchAccess(this);
Handle = context.Renderer.CreateBuffer((int)size, access);
_useGranular = size > GranularBufferThreshold;
@ -161,6 +178,29 @@ namespace Ryujinx.Graphics.Gpu.Memory
_virtualDependenciesLock = new ReaderWriterLockSlim();
}
/// <summary>
/// Recreates the backing buffer based on the desired access type
/// reported by the backing state struct.
/// </summary>
private void ChangeBacking()
{
BufferAccess access = BackingState.SwitchAccess(this);
BufferHandle newHandle = _context.Renderer.CreateBuffer((int)Size, access);
_context.Renderer.Pipeline.CopyBuffer(Handle, newHandle, 0, 0, (int)Size);
_modifiedRanges?.SelfMigration();
// If swtiching from device local to host mapped, pre-flushing data no longer makes sense.
// This is set to null and disposed when the migration fully completes.
_preFlush = null;
Handle = newHandle;
_physicalMemory.BufferCache.BufferBackingChanged(this);
}
/// <summary>
/// Gets a sub-range from the buffer, from a start address til a page boundary after the given size.
/// </summary>
@ -246,6 +286,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
else
{
BackingState.RecordSet();
_context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size));
CopyToDependantVirtualBuffers();
}
@ -283,15 +324,35 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges ??= new BufferModifiedRangeList(_context, this, Flush);
}
/// <summary>
/// Checks if a backing change is deemed necessary from the given usage.
/// If it is, queues a backing change to happen on the next sync action.
/// </summary>
/// <param name="stage">Buffer stage that can change backing type</param>
private void TryQueueBackingChange(BufferStage stage)
{
if (BackingState.ShouldChangeBacking(stage))
{
if (!_syncActionRegistered)
{
_context.RegisterSyncAction(this);
_syncActionRegistered = true;
}
}
}
/// <summary>
/// Signal that the given region of the buffer has been modified.
/// </summary>
/// <param name="address">The start address of the modified region</param>
/// <param name="size">The size of the modified region</param>
public void SignalModified(ulong address, ulong size)
/// <param name="stage">Buffer stage that triggered the modification</param>
public void SignalModified(ulong address, ulong size, BufferStage stage)
{
EnsureRangeList();
TryQueueBackingChange(stage);
_modifiedRanges.SignalModified(address, size);
if (!_syncActionRegistered)
@ -311,6 +372,37 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges?.Clear(address, size);
}
/// <summary>
/// Action to be performed immediately before sync is created.
/// This will copy any buffer ranges designated for pre-flushing.
/// </summary>
/// <param name="syncpoint">True if the action is a guest syncpoint</param>
public void SyncPreAction(bool syncpoint)
{
if (_referenceCount == 0)
{
return;
}
if (BackingState.ShouldChangeBacking())
{
ChangeBacking();
}
if (BackingState.IsDeviceLocal)
{
_preFlush ??= new BufferPreFlush(_context, this, FlushImpl);
if (_preFlush.ShouldCopy)
{
_modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, (address, size) =>
{
_preFlush.CopyModified(address, size);
});
}
}
}
/// <summary>
/// Action to be performed when a syncpoint is reached after modification.
/// This will register read/write tracking to flush the buffer from GPU when its memory is used.
@ -466,6 +558,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="mSize">Size of the modified region</param>
private void LoadRegion(ulong mAddress, ulong mSize)
{
BackingState.RecordSet();
int offset = (int)(mAddress - Address);
_context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize));
@ -539,18 +633,84 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Flushes a range of the buffer.
/// This writes the range data back into guest memory.
/// </summary>
/// <param name="handle">Buffer handle to flush data from</param>
/// <param name="address">Start address of the range</param>
/// <param name="size">Size in bytes of the range</param>
public void Flush(ulong address, ulong size)
private void FlushImpl(BufferHandle handle, ulong address, ulong size)
{
int offset = (int)(address - Address);
using PinnedSpan<byte> data = _context.Renderer.GetBufferData(Handle, offset, (int)size);
using PinnedSpan<byte> data = _context.Renderer.GetBufferData(handle, offset, (int)size);
// TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers.
_physicalMemory.WriteUntracked(address, CopyFromDependantVirtualBuffers(data.Get(), address, size));
}
/// <summary>
/// Flushes a range of the buffer.
/// This writes the range data back into guest memory.
/// </summary>
/// <param name="address">Start address of the range</param>
/// <param name="size">Size in bytes of the range</param>
private void FlushImpl(ulong address, ulong size)
{
FlushImpl(Handle, address, size);
}
/// <summary>
/// Flushes a range of the buffer from the most optimal source.
/// This writes the range data back into guest memory.
/// </summary>
/// <param name="address">Start address of the range</param>
/// <param name="size">Size in bytes of the range</param>
/// <param name="syncNumber">Sync number waited for before flushing the data</param>
public void Flush(ulong address, ulong size, ulong syncNumber)
{
BackingState.RecordFlush();
BufferPreFlush preFlush = _preFlush;
if (preFlush != null)
{
preFlush.FlushWithAction(address, size, syncNumber);
}
else
{
FlushImpl(address, size);
}
}
/// <summary>
/// Gets an action that disposes the backing buffer using its current handle.
/// Useful for deleting an old copy of the buffer after the handle changes.
/// </summary>
/// <returns>An action that flushes data from the specified range, using the buffer handle at the time the method is generated</returns>
public Action GetSnapshotDisposeAction()
{
BufferHandle handle = Handle;
BufferPreFlush preFlush = _preFlush;
return () =>
{
_context.Renderer.DeleteBuffer(handle);
preFlush?.Dispose();
};
}
/// <summary>
/// Gets an action that flushes a range of the buffer using its current handle.
/// Useful for flushing data from old copies of the buffer after the handle changes.
/// </summary>
/// <returns>An action that flushes data from the specified range, using the buffer handle at the time the method is generated</returns>
public BufferFlushAction GetSnapshotFlushAction()
{
BufferHandle handle = Handle;
return (ulong address, ulong size, ulong _) =>
{
FlushImpl(handle, address, size);
};
}
/// <summary>
/// Align a given address and size region to page boundaries.
/// </summary>
@ -857,6 +1017,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges?.Clear();
_context.Renderer.DeleteBuffer(Handle);
_preFlush?.Dispose();
_preFlush = null;
UnmappedSequence++;
}

View file

@ -0,0 +1,294 @@
using Ryujinx.Graphics.GAL;
using System;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Memory
{
/// <summary>
/// Type of backing memory.
/// In ascending order of priority when merging multiple buffer backing states.
/// </summary>
internal enum BufferBackingType
{
HostMemory,
DeviceMemory,
DeviceMemoryWithFlush
}
/// <summary>
/// Keeps track of buffer usage to decide what memory heap that buffer memory is placed on.
/// Dedicated GPUs prefer certain types of resources to be device local,
/// and if we need data to be read back, we might prefer that they're in host memory.
///
/// The measurements recorded here compare to a set of heruristics (thresholds and conditions)
/// that appear to produce good performance in most software.
/// </summary>
internal struct BufferBackingState
{
private const int DeviceLocalSizeThreshold = 256 * 1024; // 256kb
private const int SetCountThreshold = 100;
private const int WriteCountThreshold = 50;
private const int FlushCountThreshold = 5;
private const int DeviceLocalForceExpiry = 100;
public readonly bool IsDeviceLocal => _activeType != BufferBackingType.HostMemory;
private readonly SystemMemoryType _systemMemoryType;
private BufferBackingType _activeType;
private BufferBackingType _desiredType;
private bool _canSwap;
private int _setCount;
private int _writeCount;
private int _flushCount;
private int _flushTemp;
private int _lastFlushWrite;
private int _deviceLocalForceCount;
private readonly int _size;
/// <summary>
/// Initialize the buffer backing state for a given parent buffer.
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="parent">Parent buffer</param>
/// <param name="stage">Initial buffer stage</param>
/// <param name="baseBuffers">Buffers to inherit state from</param>
public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, IEnumerable<Buffer> baseBuffers = null)
{
_size = (int)parent.Size;
_systemMemoryType = context.Capabilities.MemoryType;
// Backend managed is always auto, unified memory is always host.
_desiredType = BufferBackingType.HostMemory;
_canSwap = _systemMemoryType != SystemMemoryType.BackendManaged && _systemMemoryType != SystemMemoryType.UnifiedMemory;
if (_canSwap)
{
// Might want to start certain buffers as being device local,
// and the usage might also lock those buffers into being device local.
BufferStage storageFlags = stage & BufferStage.StorageMask;
if (parent.Size > DeviceLocalSizeThreshold && baseBuffers == null)
{
_desiredType = BufferBackingType.DeviceMemory;
}
if (storageFlags != 0)
{
// Storage buffer bindings may require special treatment.
var rawStage = stage & BufferStage.StageMask;
if (rawStage == BufferStage.Fragment)
{
// Fragment read should start device local.
_desiredType = BufferBackingType.DeviceMemory;
if (storageFlags != BufferStage.StorageRead)
{
// Fragment write should stay device local until the use doesn't happen anymore.
_deviceLocalForceCount = DeviceLocalForceExpiry;
}
}
// TODO: Might be nice to force atomic access to be device local for any stage.
}
if (baseBuffers != null)
{
foreach (Buffer buffer in baseBuffers)
{
CombineState(buffer.BackingState);
}
}
}
}
/// <summary>
/// Combine buffer backing types, selecting the one with highest priority.
/// </summary>
/// <param name="left">First buffer backing type</param>
/// <param name="right">Second buffer backing type</param>
/// <returns>Combined buffer backing type</returns>
private static BufferBackingType CombineTypes(BufferBackingType left, BufferBackingType right)
{
return (BufferBackingType)Math.Max((int)left, (int)right);
}
/// <summary>
/// Combine the state from the given buffer backing state with this one,
/// so that the state isn't lost when migrating buffers.
/// </summary>
/// <param name="oldState">Buffer state to combine into this state</param>
private void CombineState(BufferBackingState oldState)
{
_setCount += oldState._setCount;
_writeCount += oldState._writeCount;
_flushCount += oldState._flushCount;
_flushTemp += oldState._flushTemp;
_lastFlushWrite = -1;
_deviceLocalForceCount = Math.Max(_deviceLocalForceCount, oldState._deviceLocalForceCount);
_canSwap &= oldState._canSwap;
_desiredType = CombineTypes(_desiredType, oldState._desiredType);
}
/// <summary>
/// Get the buffer access for the desired backing type, and record that type as now being active.
/// </summary>
/// <param name="parent">Parent buffer</param>
/// <returns>Buffer access</returns>
public BufferAccess SwitchAccess(Buffer parent)
{
BufferAccess access = parent.SparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default;
bool isBackendManaged = _systemMemoryType == SystemMemoryType.BackendManaged;
if (!isBackendManaged)
{
switch (_desiredType)
{
case BufferBackingType.HostMemory:
access |= BufferAccess.HostMemory;
break;
case BufferBackingType.DeviceMemory:
access |= BufferAccess.DeviceMemory;
break;
case BufferBackingType.DeviceMemoryWithFlush:
access |= BufferAccess.DeviceMemoryMapped;
break;
}
}
_activeType = _desiredType;
return access;
}
/// <summary>
/// Record when data has been uploaded to the buffer.
/// </summary>
public void RecordSet()
{
_setCount++;
ConsiderUseCounts();
}
/// <summary>
/// Record when data has been flushed from the buffer.
/// </summary>
public void RecordFlush()
{
if (_lastFlushWrite != _writeCount)
{
// If it's on the same page as the last flush, ignore it.
_lastFlushWrite = _writeCount;
_flushCount++;
}
}
/// <summary>
/// Determine if the buffer backing should be changed.
/// </summary>
/// <returns>True if the desired backing type is different from the current type</returns>
public readonly bool ShouldChangeBacking()
{
return _desiredType != _activeType;
}
/// <summary>
/// Determine if the buffer backing should be changed, considering a new use with the given buffer stage.
/// </summary>
/// <param name="stage">Buffer stage for the use</param>
/// <returns>True if the desired backing type is different from the current type</returns>
public bool ShouldChangeBacking(BufferStage stage)
{
if (!_canSwap)
{
return false;
}
BufferStage storageFlags = stage & BufferStage.StorageMask;
if (storageFlags != 0)
{
if (storageFlags != BufferStage.StorageRead)
{
// Storage write.
_writeCount++;
var rawStage = stage & BufferStage.StageMask;
if (rawStage == BufferStage.Fragment)
{
// Switch to device memory, swap back only if this use disappears.
_desiredType = CombineTypes(_desiredType, BufferBackingType.DeviceMemory);
_deviceLocalForceCount = DeviceLocalForceExpiry;
// TODO: Might be nice to force atomic access to be device local for any stage.
}
}
ConsiderUseCounts();
}
return _desiredType != _activeType;
}
/// <summary>
/// Evaluate the current counts to determine what the buffer's desired backing type is.
/// This method depends on heuristics devised by testing a variety of software.
/// </summary>
private void ConsiderUseCounts()
{
if (_canSwap)
{
if (_writeCount >= WriteCountThreshold || _setCount >= SetCountThreshold || _flushCount >= FlushCountThreshold)
{
if (_deviceLocalForceCount > 0 && --_deviceLocalForceCount != 0)
{
// Some buffer usage demanded that the buffer stay device local.
// The desired type was selected when this counter was set.
}
else if (_flushCount > 0 || _flushTemp-- > 0)
{
// Buffers that flush should ideally be mapped in host address space for easy copies.
// If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages).
// If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached.
_desiredType = _size > DeviceLocalSizeThreshold ? BufferBackingType.DeviceMemoryWithFlush : BufferBackingType.HostMemory;
}
else if (_writeCount >= WriteCountThreshold)
{
// Buffers that are written often should ideally be in the device local heap. (Storage buffers)
_desiredType = BufferBackingType.DeviceMemory;
}
else if (_setCount > SetCountThreshold)
{
// Buffers that have their data set often should ideally be host mapped. (Constant buffers)
_desiredType = BufferBackingType.HostMemory;
}
// It's harder for a buffer that is flushed to revert to another type of mapping.
if (_flushCount > 0)
{
_flushTemp = 1000;
}
_lastFlushWrite = -1;
_flushCount = 0;
_writeCount = 0;
_setCount = 0;
}
}
}
}
}

View file

@ -107,8 +107,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="memoryManager">GPU memory manager where the buffer is mapped</param>
/// <param name="gpuVa">Start GPU virtual address of the buffer</param>
/// <param name="size">Size in bytes of the buffer</param>
/// <param name="stage">The type of usage that created the buffer</param>
/// <returns>Contiguous physical range of the buffer, after address translation</returns>
public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size)
public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage)
{
if (gpuVa == 0)
{
@ -119,7 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (address != MemoryManager.PteUnmapped)
{
CreateBuffer(address, size);
CreateBuffer(address, size, stage);
}
return new MultiRange(address, size);
@ -132,8 +133,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="memoryManager">GPU memory manager where the buffer is mapped</param>
/// <param name="gpuVa">Start GPU virtual address of the buffer</param>
/// <param name="size">Size in bytes of the buffer</param>
/// <param name="stage">The type of usage that created the buffer</param>
/// <returns>Physical ranges of the buffer, after address translation</returns>
public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size)
public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage)
{
if (gpuVa == 0)
{
@ -149,7 +151,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
return range;
}
CreateBuffer(range);
CreateBuffer(range, stage);
return range;
}
@ -161,8 +163,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="memoryManager">GPU memory manager where the buffer is mapped</param>
/// <param name="gpuVa">Start GPU virtual address of the buffer</param>
/// <param name="size">Size in bytes of the buffer</param>
/// <param name="stage">The type of usage that created the buffer</param>
/// <returns>Physical ranges of the buffer, after address translation</returns>
public MultiRange TranslateAndCreateMultiBuffersPhysicalOnly(MemoryManager memoryManager, ulong gpuVa, ulong size)
public MultiRange TranslateAndCreateMultiBuffersPhysicalOnly(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage)
{
if (gpuVa == 0)
{
@ -186,11 +189,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
if (range.Count > 1)
{
CreateBuffer(subRange.Address, subRange.Size, SparseBufferAlignmentSize);
CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize);
}
else
{
CreateBuffer(subRange.Address, subRange.Size);
CreateBuffer(subRange.Address, subRange.Size, stage);
}
}
}
@ -203,11 +206,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// This can be used to ensure the existance of a buffer.
/// </summary>
/// <param name="range">Physical ranges of memory where the buffer data is located</param>
public void CreateBuffer(MultiRange range)
/// <param name="stage">The type of usage that created the buffer</param>
public void CreateBuffer(MultiRange range, BufferStage stage)
{
if (range.Count > 1)
{
CreateMultiRangeBuffer(range);
CreateMultiRangeBuffer(range, stage);
}
else
{
@ -215,7 +219,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (subRange.Address != MemoryManager.PteUnmapped)
{
CreateBuffer(subRange.Address, subRange.Size);
CreateBuffer(subRange.Address, subRange.Size, stage);
}
}
}
@ -226,7 +230,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
/// <param name="address">Address of the buffer in memory</param>
/// <param name="size">Size of the buffer in bytes</param>
public void CreateBuffer(ulong address, ulong size)
/// <param name="stage">The type of usage that created the buffer</param>
public void CreateBuffer(ulong address, ulong size, BufferStage stage)
{
ulong endAddress = address + size;
@ -239,7 +244,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
alignedEndAddress += BufferAlignmentSize;
}
CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress);
CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage);
}
/// <summary>
@ -248,8 +253,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
/// <param name="address">Address of the buffer in memory</param>
/// <param name="size">Size of the buffer in bytes</param>
/// <param name="stage">The type of usage that created the buffer</param>
/// <param name="alignment">Alignment of the start address of the buffer in bytes</param>
public void CreateBuffer(ulong address, ulong size, ulong alignment)
public void CreateBuffer(ulong address, ulong size, BufferStage stage, ulong alignment)
{
ulong alignmentMask = alignment - 1;
ulong pageAlignmentMask = BufferAlignmentMask;
@ -264,7 +270,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
alignedEndAddress += pageAlignmentMask;
}
CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, alignment);
CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage, alignment);
}
/// <summary>
@ -272,7 +278,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// if it does not exist yet.
/// </summary>
/// <param name="range">Physical ranges of memory</param>
private void CreateMultiRangeBuffer(MultiRange range)
/// <param name="stage">The type of usage that created the buffer</param>
private void CreateMultiRangeBuffer(MultiRange range, BufferStage stage)
{
// Ensure all non-contiguous buffer we might use are sparse aligned.
for (int i = 0; i < range.Count; i++)
@ -281,7 +288,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (subRange.Address != MemoryManager.PteUnmapped)
{
CreateBuffer(subRange.Address, subRange.Size, SparseBufferAlignmentSize);
CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize);
}
}
@ -431,9 +438,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
result.EndGpuAddress < gpuVa + size ||
result.UnmappedSequence != result.Buffer.UnmappedSequence)
{
MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size);
MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size, BufferStage.Internal);
ulong address = range.GetSubRange(0).Address;
result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size));
result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size, BufferStage.Internal));
_dirtyCache[gpuVa] = result;
}
@ -466,9 +473,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
result.EndGpuAddress < alignedEndGpuVa ||
result.UnmappedSequence != result.Buffer.UnmappedSequence)
{
MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size);
MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size, BufferStage.None);
ulong address = range.GetSubRange(0).Address;
result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size));
result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size, BufferStage.None));
_modifiedCache[alignedGpuVa] = result;
}
@ -485,7 +492,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
/// <param name="address">Address of the buffer in guest memory</param>
/// <param name="size">Size in bytes of the buffer</param>
private void CreateBufferAligned(ulong address, ulong size)
/// <param name="stage">The type of usage that created the buffer</param>
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage)
{
Buffer[] overlaps = _bufferOverlaps;
int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps);
@ -546,13 +554,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong newSize = endAddress - address;
CreateBufferAligned(address, newSize, anySparseCompatible, overlaps, overlapsCount);
CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlaps, overlapsCount);
}
}
else
{
// No overlap, just create a new buffer.
Buffer buffer = new(_context, _physicalMemory, address, size, sparseCompatible: false);
Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false);
lock (_buffers)
{
@ -570,8 +578,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
/// <param name="address">Address of the buffer in guest memory</param>
/// <param name="size">Size in bytes of the buffer</param>
/// <param name="stage">The type of usage that created the buffer</param>
/// <param name="alignment">Alignment of the start address of the buffer</param>
private void CreateBufferAligned(ulong address, ulong size, ulong alignment)
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment)
{
Buffer[] overlaps = _bufferOverlaps;
int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps);
@ -624,13 +633,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong newSize = endAddress - address;
CreateBufferAligned(address, newSize, sparseAligned, overlaps, overlapsCount);
CreateBufferAligned(address, newSize, stage, sparseAligned, overlaps, overlapsCount);
}
}
else
{
// No overlap, just create a new buffer.
Buffer buffer = new(_context, _physicalMemory, address, size, sparseAligned);
Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseAligned);
lock (_buffers)
{
@ -648,12 +657,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
/// <param name="address">Address of the buffer in guest memory</param>
/// <param name="size">Size in bytes of the buffer</param>
/// <param name="stage">The type of usage that created the buffer</param>
/// <param name="sparseCompatible">Indicates if the buffer can be used in a sparse buffer mapping</param>
/// <param name="overlaps">Buffers overlapping the range</param>
/// <param name="overlapsCount">Total of overlaps</param>
private void CreateBufferAligned(ulong address, ulong size, bool sparseCompatible, Buffer[] overlaps, int overlapsCount)
private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps, int overlapsCount)
{
Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, sparseCompatible, overlaps.Take(overlapsCount));
Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps.Take(overlapsCount));
lock (_buffers)
{
@ -704,7 +714,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (int index = 0; index < overlapCount; index++)
{
CreateMultiRangeBuffer(overlaps[index].Range);
CreateMultiRangeBuffer(overlaps[index].Range, BufferStage.None);
}
}
@ -731,8 +741,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="size">Size in bytes of the copy</param>
public void CopyBuffer(MemoryManager memoryManager, ulong srcVa, ulong dstVa, ulong size)
{
MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size);
MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size);
MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy);
MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size, BufferStage.Copy);
if (srcRange.Count == 1 && dstRange.Count == 1)
{
@ -788,8 +798,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="size">Size in bytes of the copy</param>
private void CopyBufferSingleRange(MemoryManager memoryManager, ulong srcAddress, ulong dstAddress, ulong size)
{
Buffer srcBuffer = GetBuffer(srcAddress, size);
Buffer dstBuffer = GetBuffer(dstAddress, size);
Buffer srcBuffer = GetBuffer(srcAddress, size, BufferStage.Copy);
Buffer dstBuffer = GetBuffer(dstAddress, size, BufferStage.Copy);
int srcOffset = (int)(srcAddress - srcBuffer.Address);
int dstOffset = (int)(dstAddress - dstBuffer.Address);
@ -803,7 +813,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (srcBuffer.IsModified(srcAddress, size))
{
dstBuffer.SignalModified(dstAddress, size);
dstBuffer.SignalModified(dstAddress, size, BufferStage.Copy);
}
else
{
@ -828,12 +838,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="value">Value to be written into the buffer</param>
public void ClearBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, uint value)
{
MultiRange range = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, gpuVa, size);
MultiRange range = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, gpuVa, size, BufferStage.Copy);
for (int index = 0; index < range.Count; index++)
{
MemoryRange subRange = range.GetSubRange(index);
Buffer buffer = GetBuffer(subRange.Address, subRange.Size);
Buffer buffer = GetBuffer(subRange.Address, subRange.Size, BufferStage.Copy);
int offset = (int)(subRange.Address - buffer.Address);
@ -849,18 +859,19 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Gets a buffer sub-range starting at a given memory address, aligned to the next page boundary.
/// </summary>
/// <param name="range">Physical regions of memory where the buffer is mapped</param>
/// <param name="stage">Buffer stage that triggered the access</param>
/// <param name="write">Whether the buffer will be written to by this use</param>
/// <returns>The buffer sub-range starting at the given memory address</returns>
public BufferRange GetBufferRangeAligned(MultiRange range, bool write = false)
public BufferRange GetBufferRangeAligned(MultiRange range, BufferStage stage, bool write = false)
{
if (range.Count > 1)
{
return GetBuffer(range, write).GetRange(range);
return GetBuffer(range, stage, write).GetRange(range);
}
else
{
MemoryRange subRange = range.GetSubRange(0);
return GetBuffer(subRange.Address, subRange.Size, write).GetRangeAligned(subRange.Address, subRange.Size, write);
return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRangeAligned(subRange.Address, subRange.Size, write);
}
}
@ -868,18 +879,19 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Gets a buffer sub-range for a given memory range.
/// </summary>
/// <param name="range">Physical regions of memory where the buffer is mapped</param>
/// <param name="stage">Buffer stage that triggered the access</param>
/// <param name="write">Whether the buffer will be written to by this use</param>
/// <returns>The buffer sub-range for the given range</returns>
public BufferRange GetBufferRange(MultiRange range, bool write = false)
public BufferRange GetBufferRange(MultiRange range, BufferStage stage, bool write = false)
{
if (range.Count > 1)
{
return GetBuffer(range, write).GetRange(range);
return GetBuffer(range, stage, write).GetRange(range);
}
else
{
MemoryRange subRange = range.GetSubRange(0);
return GetBuffer(subRange.Address, subRange.Size, write).GetRange(subRange.Address, subRange.Size, write);
return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRange(subRange.Address, subRange.Size, write);
}
}
@ -888,9 +900,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// A buffer overlapping with the specified range is assumed to already exist on the cache.
/// </summary>
/// <param name="range">Physical regions of memory where the buffer is mapped</param>
/// <param name="stage">Buffer stage that triggered the access</param>
/// <param name="write">Whether the buffer will be written to by this use</param>
/// <returns>The buffer where the range is fully contained</returns>
private MultiRangeBuffer GetBuffer(MultiRange range, bool write = false)
private MultiRangeBuffer GetBuffer(MultiRange range, BufferStage stage, bool write = false)
{
for (int i = 0; i < range.Count; i++)
{
@ -902,7 +915,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (write)
{
subBuffer.SignalModified(subRange.Address, subRange.Size);
subBuffer.SignalModified(subRange.Address, subRange.Size, stage);
}
}
@ -935,9 +948,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
/// <param name="address">Start address of the memory range</param>
/// <param name="size">Size in bytes of the memory range</param>
/// <param name="stage">Buffer stage that triggered the access</param>
/// <param name="write">Whether the buffer will be written to by this use</param>
/// <returns>The buffer where the range is fully contained</returns>
private Buffer GetBuffer(ulong address, ulong size, bool write = false)
private Buffer GetBuffer(ulong address, ulong size, BufferStage stage, bool write = false)
{
Buffer buffer;
@ -950,7 +964,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (write)
{
buffer.SignalModified(address, size);
buffer.SignalModified(address, size, stage);
}
}
else
@ -1004,6 +1018,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>
/// Signal that the given buffer's handle has changed,
/// forcing rebind and any overlapping multi-range buffers to be recreated.
/// </summary>
/// <param name="buffer">The buffer that has changed handle</param>
public void BufferBackingChanged(Buffer buffer)
{
NotifyBuffersModified?.Invoke();
RecreateMultiRangeBuffers(buffer.Address, buffer.Size);
}
/// <summary>
/// Prune any invalid entries from a quick access dictionary.
/// </summary>

View file

@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="type">Type of each index buffer element</param>
public void SetIndexBuffer(ulong gpuVa, ulong size, IndexType type)
{
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.IndexBuffer);
_indexBuffer.Range = range;
_indexBuffer.Type = type;
@ -186,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="divisor">Vertex divisor of the buffer, for instanced draws</param>
public void SetVertexBuffer(int index, ulong gpuVa, ulong size, int stride, int divisor)
{
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.VertexBuffer);
_vertexBuffers[index].Range = range;
_vertexBuffers[index].Stride = stride;
@ -213,7 +213,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="size">Size in bytes of the transform feedback buffer</param>
public void SetTransformFeedbackBuffer(int index, ulong gpuVa, ulong size)
{
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStage.TransformFeedback);
_transformFeedbackBuffers[index] = new BufferBounds(range);
_transformFeedbackBuffersDirty = true;
@ -260,7 +260,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
gpuVa = BitUtils.AlignDown<ulong>(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags));
_cpStorageBuffers.SetBounds(index, range, flags);
}
@ -284,7 +284,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
gpuVa = BitUtils.AlignDown<ulong>(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags));
if (!buffers.Buffers[index].Range.Equals(range))
{
@ -303,7 +303,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="size">Size in bytes of the storage buffer</param>
public void SetComputeUniformBuffer(int index, ulong gpuVa, ulong size)
{
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.Compute);
_cpUniformBuffers.SetBounds(index, range);
}
@ -318,7 +318,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="size">Size in bytes of the storage buffer</param>
public void SetGraphicsUniformBuffer(int stage, int index, ulong gpuVa, ulong size)
{
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStageUtils.FromShaderStage(stage));
_gpUniformBuffers[stage].SetBounds(index, range);
_gpUniformBuffersDirty = true;
@ -502,7 +502,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
foreach (var binding in _bufferTextures)
{
var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore);
var range = bufferCache.GetBufferRange(binding.Range, isStore);
var range = bufferCache.GetBufferRange(binding.Range, BufferStageUtils.TextureBuffer(binding.Stage, binding.BindingInfo.Flags), isStore);
binding.Texture.SetStorage(range);
// The texture must be rebound to use the new storage if it was updated.
@ -526,7 +526,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
foreach (var binding in _bufferTextureArrays)
{
var range = bufferCache.GetBufferRange(binding.Range);
var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None);
binding.Texture.SetStorage(range);
textureArray[0] = binding.Texture;
@ -536,7 +536,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
foreach (var binding in _bufferImageArrays)
{
var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore);
var range = bufferCache.GetBufferRange(binding.Range, isStore);
var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None, isStore);
binding.Texture.SetStorage(range);
textureArray[0] = binding.Texture;
@ -565,7 +565,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (!_indexBuffer.Range.IsUnmapped)
{
BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range);
BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range, BufferStage.IndexBuffer);
_context.Renderer.Pipeline.SetIndexBuffer(buffer, _indexBuffer.Type);
}
@ -597,7 +597,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
continue;
}
BufferRange buffer = bufferCache.GetBufferRange(vb.Range);
BufferRange buffer = bufferCache.GetBufferRange(vb.Range, BufferStage.VertexBuffer);
vertexBuffers[index] = new VertexBufferDescriptor(buffer, vb.Stride, vb.Divisor);
}
@ -637,7 +637,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
continue;
}
tfbs[index] = bufferCache.GetBufferRange(tfb.Range, write: true);
tfbs[index] = bufferCache.GetBufferRange(tfb.Range, BufferStage.TransformFeedback, write: true);
}
_context.Renderer.Pipeline.SetTransformFeedbackBuffers(tfbs);
@ -684,7 +684,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
_context.SupportBufferUpdater.SetTfeOffset(index, tfeOffset);
buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, write: true));
buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, BufferStage.TransformFeedback, write: true));
}
}
@ -751,6 +751,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (ShaderStage stage = ShaderStage.Vertex; stage <= ShaderStage.Fragment; stage++)
{
ref var buffers = ref bindings[(int)stage - 1];
BufferStage bufferStage = BufferStageUtils.FromShaderStage(stage);
for (int index = 0; index < buffers.Count; index++)
{
@ -762,8 +763,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write);
var range = isStorage
? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite)
: bufferCache.GetBufferRange(bounds.Range);
? bufferCache.GetBufferRangeAligned(bounds.Range, bufferStage | BufferStageUtils.FromUsage(bounds.Flags), isWrite)
: bufferCache.GetBufferRange(bounds.Range, bufferStage);
ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range);
}
@ -799,8 +800,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write);
var range = isStorage
? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite)
: bufferCache.GetBufferRange(bounds.Range);
? bufferCache.GetBufferRangeAligned(bounds.Range, BufferStageUtils.ComputeStorage(bounds.Flags), isWrite)
: bufferCache.GetBufferRange(bounds.Range, BufferStage.Compute);
ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range);
}
@ -875,7 +876,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
Format format,
bool isImage)
{
_channel.MemoryManager.Physical.BufferCache.CreateBuffer(range);
_channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags));
_bufferTextures.Add(new BufferTextureBinding(stage, texture, range, bindingInfo, format, isImage));
}
@ -883,6 +884,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary>
/// Sets the buffer storage of a buffer texture array element. This will be bound when the buffer manager commits bindings.
/// </summary>
/// <param name="stage">Shader stage accessing the texture</param>
/// <param name="array">Texture array where the element will be inserted</param>
/// <param name="texture">Buffer texture</param>
/// <param name="range">Physical ranges of memory where the buffer texture data is located</param>
@ -890,6 +892,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="index">Index of the binding on the array</param>
/// <param name="format">Format of the buffer texture</param>
public void SetBufferTextureStorage(
ShaderStage stage,
ITextureArray array,
ITexture texture,
MultiRange range,
@ -897,7 +900,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
int index,
Format format)
{
_channel.MemoryManager.Physical.BufferCache.CreateBuffer(range);
_channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags));
_bufferTextureArrays.Add(new BufferTextureArrayBinding<ITextureArray>(array, texture, range, bindingInfo, index, format));
}
@ -905,6 +908,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary>
/// Sets the buffer storage of a buffer image array element. This will be bound when the buffer manager commits bindings.
/// </summary>
/// <param name="stage">Shader stage accessing the texture</param>
/// <param name="array">Image array where the element will be inserted</param>
/// <param name="texture">Buffer texture</param>
/// <param name="range">Physical ranges of memory where the buffer texture data is located</param>
@ -912,6 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="index">Index of the binding on the array</param>
/// <param name="format">Format of the buffer texture</param>
public void SetBufferTextureStorage(
ShaderStage stage,
IImageArray array,
ITexture texture,
MultiRange range,
@ -919,7 +924,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
int index,
Format format)
{
_channel.MemoryManager.Physical.BufferCache.CreateBuffer(range);
_channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags));
_bufferImageArrays.Add(new BufferTextureArrayBinding<IImageArray>(array, texture, range, bindingInfo, index, format));
}

View file

@ -1,37 +1,21 @@
using System;
using System.Threading;
namespace Ryujinx.Graphics.Gpu.Memory
{
/// <summary>
/// A record of when buffer data was copied from one buffer to another, along with the SyncNumber when the migration will be complete.
/// Keeps the source buffer alive for data flushes until the migration is complete.
/// A record of when buffer data was copied from multiple buffers to one migration target,
/// along with the SyncNumber when the migration will be complete.
/// Keeps the source buffers alive for data flushes until the migration is complete.
/// All spans cover the full range of the "destination" buffer.
/// </summary>
internal class BufferMigration : IDisposable
{
/// <summary>
/// The offset for the migrated region.
/// Ranges from source buffers that were copied as part of this migration.
/// Ordered by increasing base address.
/// </summary>
private readonly ulong _offset;
/// <summary>
/// The size for the migrated region.
/// </summary>
private readonly ulong _size;
/// <summary>
/// The buffer that was migrated from.
/// </summary>
private readonly Buffer _buffer;
/// <summary>
/// The source range action, to be called on overlap with an unreached sync number.
/// </summary>
private readonly Action<ulong, ulong> _sourceRangeAction;
/// <summary>
/// The source range list.
/// </summary>
private readonly BufferModifiedRangeList _source;
public BufferMigrationSpan[] Spans { get; private set; }
/// <summary>
/// The destination range list. This range list must be updated when flushing the source.
@ -43,55 +27,193 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
public readonly ulong SyncNumber;
/// <summary>
/// Number of active users there are traversing this migration's spans.
/// </summary>
private int _refCount;
/// <summary>
/// Create a new buffer migration.
/// </summary>
/// <param name="spans">Source spans for the migration</param>
/// <param name="destination">Destination buffer range list</param>
/// <param name="syncNumber">Sync number where this migration will be complete</param>
public BufferMigration(BufferMigrationSpan[] spans, BufferModifiedRangeList destination, ulong syncNumber)
{
Spans = spans;
Destination = destination;
SyncNumber = syncNumber;
}
/// <summary>
/// Add a span to the migration. Allocates a new array with the target size, and replaces it.
/// </summary>
/// <remarks>
/// The base address for the span is assumed to be higher than all other spans in the migration,
/// to keep the span array ordered.
/// </remarks>
public void AddSpanToEnd(BufferMigrationSpan span)
{
BufferMigrationSpan[] oldSpans = Spans;
BufferMigrationSpan[] newSpans = new BufferMigrationSpan[oldSpans.Length + 1];
oldSpans.CopyTo(newSpans, 0);
newSpans[oldSpans.Length] = span;
Spans = newSpans;
}
/// <summary>
/// Performs the given range action, or one from a migration that overlaps and has not synced yet.
/// </summary>
/// <param name="offset">The offset to pass to the action</param>
/// <param name="size">The size to pass to the action</param>
/// <param name="syncNumber">The sync number that has been reached</param>
/// <param name="rangeAction">The action to perform</param>
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction)
{
long syncDiff = (long)(syncNumber - SyncNumber);
if (syncDiff >= 0)
{
// The migration has completed. Run the parent action.
rangeAction(offset, size, syncNumber);
}
else
{
Interlocked.Increment(ref _refCount);
ulong prevAddress = offset;
ulong endAddress = offset + size;
foreach (BufferMigrationSpan span in Spans)
{
if (!span.Overlaps(offset, size))
{
continue;
}
if (span.Address > prevAddress)
{
// There's a gap between this span and the last (or the start address). Flush the range using the parent action.
rangeAction(prevAddress, span.Address - prevAddress, syncNumber);
}
span.RangeActionWithMigration(offset, size, syncNumber);
prevAddress = span.Address + span.Size;
}
if (endAddress > prevAddress)
{
// There's a gap at the end of the range with no migration. Flush the range using the parent action.
rangeAction(prevAddress, endAddress - prevAddress, syncNumber);
}
Interlocked.Decrement(ref _refCount);
}
}
/// <summary>
/// Dispose the buffer migration. This removes the reference from the destination range list,
/// and runs all the dispose buffers for the migration spans. (typically disposes the source buffer)
/// </summary>
public void Dispose()
{
while (Volatile.Read(ref _refCount) > 0)
{
// Coming into this method, the sync for the migration will be met, so nothing can increment the ref count.
// However, an existing traversal of the spans for data flush could still be in progress.
// Spin if this is ever the case, so they don't get disposed before the operation is complete.
}
Destination.RemoveMigration(this);
foreach (BufferMigrationSpan span in Spans)
{
span.Dispose();
}
}
}
/// <summary>
/// A record of when buffer data was copied from one buffer to another, for a specific range in a source buffer.
/// Keeps the source buffer alive for data flushes until the migration is complete.
/// </summary>
internal readonly struct BufferMigrationSpan : IDisposable
{
/// <summary>
/// The offset for the migrated region.
/// </summary>
public readonly ulong Address;
/// <summary>
/// The size for the migrated region.
/// </summary>
public readonly ulong Size;
/// <summary>
/// The action to perform when the migration isn't needed anymore.
/// </summary>
private readonly Action _disposeAction;
/// <summary>
/// The source range action, to be called on overlap with an unreached sync number.
/// </summary>
private readonly BufferFlushAction _sourceRangeAction;
/// <summary>
/// Optional migration for the source data. Can chain together if many migrations happen in a short time.
/// If this is null, then _sourceRangeAction will always provide up to date data.
/// </summary>
private readonly BufferMigration _source;
/// <summary>
/// Creates a record for a buffer migration.
/// </summary>
/// <param name="buffer">The source buffer for this migration</param>
/// <param name="disposeAction">The action to perform when the migration isn't needed anymore</param>
/// <param name="sourceRangeAction">The flush action for the source buffer</param>
/// <param name="source">The modified range list for the source buffer</param>
/// <param name="dest">The modified range list for the destination buffer</param>
/// <param name="syncNumber">The sync number for when the migration is complete</param>
public BufferMigration(
/// <param name="source">Pending migration for the source buffer</param>
public BufferMigrationSpan(
Buffer buffer,
Action<ulong, ulong> sourceRangeAction,
BufferModifiedRangeList source,
BufferModifiedRangeList dest,
ulong syncNumber)
Action disposeAction,
BufferFlushAction sourceRangeAction,
BufferMigration source)
{
_offset = buffer.Address;
_size = buffer.Size;
_buffer = buffer;
Address = buffer.Address;
Size = buffer.Size;
_disposeAction = disposeAction;
_sourceRangeAction = sourceRangeAction;
_source = source;
Destination = dest;
SyncNumber = syncNumber;
}
/// <summary>
/// Creates a record for a buffer migration, using the default buffer dispose action.
/// </summary>
/// <param name="buffer">The source buffer for this migration</param>
/// <param name="sourceRangeAction">The flush action for the source buffer</param>
/// <param name="source">Pending migration for the source buffer</param>
public BufferMigrationSpan(
Buffer buffer,
BufferFlushAction sourceRangeAction,
BufferMigration source) : this(buffer, buffer.DecrementReferenceCount, sourceRangeAction, source) { }
/// <summary>
/// Determine if the given range overlaps this migration, and has not been completed yet.
/// </summary>
/// <param name="offset">Start offset</param>
/// <param name="size">Range size</param>
/// <param name="syncNumber">The sync number that was waited on</param>
/// <returns>True if overlapping and in progress, false otherwise</returns>
public bool Overlaps(ulong offset, ulong size, ulong syncNumber)
public bool Overlaps(ulong offset, ulong size)
{
ulong end = offset + size;
ulong destEnd = _offset + _size;
long syncDiff = (long)(syncNumber - SyncNumber); // syncNumber is less if the copy has not completed.
ulong destEnd = Address + Size;
return !(end <= _offset || offset >= destEnd) && syncDiff < 0;
}
/// <summary>
/// Determine if the given range matches this migration.
/// </summary>
/// <param name="offset">Start offset</param>
/// <param name="size">Range size</param>
/// <returns>True if the range exactly matches, false otherwise</returns>
public bool FullyMatches(ulong offset, ulong size)
{
return _offset == offset && _size == size;
return !(end <= Address || offset >= destEnd);
}
/// <summary>
@ -100,26 +222,30 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="offset">Start offset</param>
/// <param name="size">Range size</param>
/// <param name="syncNumber">Current sync number</param>
/// <param name="parent">The modified range list that originally owned this range</param>
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent)
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber)
{
ulong end = offset + size;
end = Math.Min(_offset + _size, end);
offset = Math.Max(_offset, offset);
end = Math.Min(Address + Size, end);
offset = Math.Max(Address, offset);
size = end - offset;
_source.RangeActionWithMigration(offset, size, syncNumber, parent, _sourceRangeAction);
if (_source != null)
{
_source.RangeActionWithMigration(offset, size, syncNumber, _sourceRangeAction);
}
else
{
_sourceRangeAction(offset, size, syncNumber);
}
}
/// <summary>
/// Removes this reference to the range list, potentially allowing for the source buffer to be disposed.
/// Removes this migration span, potentially allowing for the source buffer to be disposed.
/// </summary>
public void Dispose()
{
Destination.RemoveMigration(this);
_buffer.DecrementReferenceCount();
_disposeAction();
}
}
}

View file

@ -1,7 +1,6 @@
using Ryujinx.Common.Pools;
using Ryujinx.Memory.Range;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Graphics.Gpu.Memory
@ -72,10 +71,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
private readonly GpuContext _context;
private readonly Buffer _parent;
private readonly Action<ulong, ulong> _flushAction;
private readonly BufferFlushAction _flushAction;
private List<BufferMigration> _sources;
private BufferMigration _migrationTarget;
private BufferMigration _source;
private BufferModifiedRangeList _migrationTarget;
private readonly object _lock = new();
@ -99,7 +98,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="context">GPU context that the buffer range list belongs to</param>
/// <param name="parent">The parent buffer that owns this range list</param>
/// <param name="flushAction">The flush action for the parent buffer</param>
public BufferModifiedRangeList(GpuContext context, Buffer parent, Action<ulong, ulong> flushAction) : base(BackingInitialSize)
public BufferModifiedRangeList(GpuContext context, Buffer parent, BufferFlushAction flushAction) : base(BackingInitialSize)
{
_context = context;
_parent = parent;
@ -199,6 +198,36 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>
/// Gets modified ranges within the specified region, and then fires the given action for each range individually.
/// </summary>
/// <param name="address">Start address to query</param>
/// <param name="size">Size to query</param>
/// <param name="syncNumber">Sync number required for a range to be signalled</param>
/// <param name="rangeAction">The action to call for each modified range</param>
public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action<ulong, ulong> rangeAction)
{
int count = 0;
ref var overlaps = ref ThreadStaticArray<BufferModifiedRange>.Get();
// Range list must be consistent for this operation.
lock (_lock)
{
count = FindOverlapsNonOverlapping(address, size, ref overlaps);
}
for (int i = 0; i < count; i++)
{
BufferModifiedRange overlap = overlaps[i];
if (overlap.SyncNumber == syncNumber)
{
rangeAction(overlap.Address, overlap.Size);
}
}
}
/// <summary>
/// Gets modified ranges within the specified region, and then fires the given action for each range individually.
/// </summary>
@ -245,41 +274,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <param name="offset">The offset to pass to the action</param>
/// <param name="size">The size to pass to the action</param>
/// <param name="syncNumber">The sync number that has been reached</param>
/// <param name="parent">The modified range list that originally owned this range</param>
/// <param name="rangeAction">The action to perform</param>
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent, Action<ulong, ulong> rangeAction)
public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction)
{
bool firstSource = true;
if (parent != this)
if (_source != null)
{
lock (_lock)
{
if (_sources != null)
{
foreach (BufferMigration source in _sources)
{
if (source.Overlaps(offset, size, syncNumber))
{
if (firstSource && !source.FullyMatches(offset, size))
{
// Perform this buffer's action first. The migrations will run after.
rangeAction(offset, size);
_source.RangeActionWithMigration(offset, size, syncNumber, rangeAction);
}
source.RangeActionWithMigration(offset, size, syncNumber, parent);
firstSource = false;
}
}
}
}
}
if (firstSource)
else
{
// No overlapping migrations, or they are not meant for this range, flush the data using the given action.
rangeAction(offset, size);
rangeAction(offset, size, syncNumber);
}
}
@ -319,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
ClearPart(overlap, clampAddress, clampEnd);
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, overlap.Parent, _flushAction);
RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
}
}
@ -329,7 +333,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
// There is a migration target to call instead. This can't be changed after set so accessing it outside the lock is fine.
_migrationTarget.Destination.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
_migrationTarget.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
}
/// <summary>
@ -367,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (rangeCount == -1)
{
_migrationTarget.Destination.WaitForAndFlushRanges(address, size);
_migrationTarget.WaitForAndFlushRanges(address, size);
return;
}
@ -407,6 +411,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary>
/// Inherit ranges from another modified range list.
/// </summary>
/// <remarks>
/// Assumes that ranges will be inherited in address ascending order.
/// </remarks>
/// <param name="ranges">The range list to inherit from</param>
/// <param name="registerRangeAction">The action to call for each modified range</param>
public void InheritRanges(BufferModifiedRangeList ranges, Action<ulong, ulong> registerRangeAction)
@ -415,18 +422,31 @@ namespace Ryujinx.Graphics.Gpu.Memory
lock (ranges._lock)
{
BufferMigration migration = new(ranges._parent, ranges._flushAction, ranges, this, _context.SyncNumber);
ranges._parent.IncrementReferenceCount();
ranges._migrationTarget = migration;
_context.RegisterBufferMigration(migration);
inheritRanges = ranges.ToArray();
lock (_lock)
{
(_sources ??= new List<BufferMigration>()).Add(migration);
// Copy over the migration from the previous range list
BufferMigration oldMigration = ranges._source;
BufferMigrationSpan span = new BufferMigrationSpan(ranges._parent, ranges._flushAction, oldMigration);
ranges._parent.IncrementReferenceCount();
if (_source == null)
{
// Create a new migration.
_source = new BufferMigration(new BufferMigrationSpan[] { span }, this, _context.SyncNumber);
_context.RegisterBufferMigration(_source);
}
else
{
// Extend the migration
_source.AddSpanToEnd(span);
}
ranges._migrationTarget = this;
foreach (BufferModifiedRange range in inheritRanges)
{
@ -445,6 +465,27 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
/// <summary>
/// Register a migration from previous buffer storage. This migration is from a snapshot of the buffer's
/// current handle to its handle in the future, and is assumed to be complete when the sync action completes.
/// When the migration completes, the handle is disposed.
/// </summary>
public void SelfMigration()
{
lock (_lock)
{
BufferMigrationSpan span = new(_parent, _parent.GetSnapshotDisposeAction(), _parent.GetSnapshotFlushAction(), _source);
BufferMigration migration = new(new BufferMigrationSpan[] { span }, this, _context.SyncNumber);
// Migration target is used to redirect flush actions to the latest range list,
// so we don't need to set it here. (this range list is still the latest)
_context.RegisterBufferMigration(migration);
_source = migration;
}
}
/// <summary>
/// Removes a source buffer migration, indicating its copy has completed.
/// </summary>
@ -453,7 +494,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
lock (_lock)
{
_sources.Remove(migration);
if (_source == migration)
{
_source = null;
}
}
}

View file

@ -0,0 +1,295 @@
using Ryujinx.Common;
using Ryujinx.Graphics.GAL;
using System;
namespace Ryujinx.Graphics.Gpu.Memory
{
/// <summary>
/// Manages flushing ranges from buffers in advance for easy access, if they are flushed often.
/// Typically, from device local memory to a host mapped target for cached access.
/// </summary>
internal class BufferPreFlush : IDisposable
{
private const ulong PageSize = MemoryManager.PageSize;
/// <summary>
/// Threshold for the number of copies without a flush required to disable preflush on a page.
/// </summary>
private const int DeactivateCopyThreshold = 200;
/// <summary>
/// Value that indicates whether a page has been flushed or copied before.
/// </summary>
private enum PreFlushState
{
None,
HasFlushed,
HasCopied
}
/// <summary>
/// Flush state for each page of the buffer.
/// Controls whether data should be copied to the flush buffer, what sync is expected
/// and unflushed copy counting for stopping copies that are no longer needed.
/// </summary>
private struct PreFlushPage
{
public PreFlushState State;
public ulong FirstActivatedSync;
public ulong LastCopiedSync;
public int CopyCount;
}
/// <summary>
/// True if there are ranges that should copy to the flush buffer, false otherwise.
/// </summary>
public bool ShouldCopy { get; private set; }
private readonly GpuContext _context;
private readonly Buffer _buffer;
private readonly PreFlushPage[] _pages;
private readonly ulong _address;
private readonly ulong _size;
private readonly ulong _misalignment;
private readonly Action<BufferHandle, ulong, ulong> _flushAction;
private BufferHandle _flushBuffer;
public BufferPreFlush(GpuContext context, Buffer parent, Action<BufferHandle, ulong, ulong> flushAction)
{
_context = context;
_buffer = parent;
_address = parent.Address;
_size = parent.Size;
_pages = new PreFlushPage[BitUtils.DivRoundUp(_size, PageSize)];
_misalignment = _address & (PageSize - 1);
_flushAction = flushAction;
}
/// <summary>
/// Ensure that the flush buffer exists.
/// </summary>
private void EnsureFlushBuffer()
{
if (_flushBuffer == BufferHandle.Null)
{
_flushBuffer = _context.Renderer.CreateBuffer((int)_size, BufferAccess.HostMemory);
}
}
/// <summary>
/// Gets a page range from an address and size byte range.
/// </summary>
/// <param name="address">Range address</param>
/// <param name="size">Range size</param>
/// <returns>A page index and count</returns>
private (int index, int count) GetPageRange(ulong address, ulong size)
{
ulong offset = address - _address;
ulong endOffset = offset + size;
int basePage = (int)(offset / PageSize);
int endPage = (int)((endOffset - 1) / PageSize);
return (basePage, 1 + endPage - basePage);
}
/// <summary>
/// Gets an offset and size range in the parent buffer from a page index and count.
/// </summary>
/// <param name="startPage">Range start page</param>
/// <param name="count">Range page count</param>
/// <returns>Offset and size range</returns>
private (int offset, int size) GetOffset(int startPage, int count)
{
int offset = (int)((ulong)startPage * PageSize - _misalignment);
int endOffset = (int)((ulong)(startPage + count) * PageSize - _misalignment);
offset = Math.Max(0, offset);
endOffset = Math.Min((int)_size, endOffset);
return (offset, endOffset - offset);
}
/// <summary>
/// Copy a range of pages from the parent buffer into the flush buffer.
/// </summary>
/// <param name="startPage">Range start page</param>
/// <param name="count">Range page count</param>
private void CopyPageRange(int startPage, int count)
{
(int offset, int size) = GetOffset(startPage, count);
EnsureFlushBuffer();
_context.Renderer.Pipeline.CopyBuffer(_buffer.Handle, _flushBuffer, offset, offset, size);
}
/// <summary>
/// Copy a modified range into the flush buffer if it's marked as flushed.
/// Any pages the range overlaps are copied, and copies aren't repeated in the same sync number.
/// </summary>
/// <param name="address">Range address</param>
/// <param name="size">Range size</param>
public void CopyModified(ulong address, ulong size)
{
(int baseIndex, int count) = GetPageRange(address, size);
ulong syncNumber = _context.SyncNumber;
int startPage = -1;
for (int i = 0; i < count; i++)
{
int pageIndex = baseIndex + i;
ref PreFlushPage page = ref _pages[pageIndex];
if (page.State > PreFlushState.None)
{
// Perform the copy, and update the state of each page.
if (startPage == -1)
{
startPage = pageIndex;
}
if (page.State != PreFlushState.HasCopied)
{
page.FirstActivatedSync = syncNumber;
page.State = PreFlushState.HasCopied;
}
else if (page.CopyCount++ >= DeactivateCopyThreshold)
{
page.CopyCount = 0;
page.State = PreFlushState.None;
}
if (page.LastCopiedSync != syncNumber)
{
page.LastCopiedSync = syncNumber;
}
}
else if (startPage != -1)
{
CopyPageRange(startPage, pageIndex - startPage);
startPage = -1;
}
}
if (startPage != -1)
{
CopyPageRange(startPage, (baseIndex + count) - startPage);
}
}
/// <summary>
/// Flush the given page range back into guest memory, optionally using data from the flush buffer.
/// The actual flushed range is an intersection of the page range and the address range.
/// </summary>
/// <param name="address">Address range start</param>
/// <param name="size">Address range size</param>
/// <param name="startPage">Page range start</param>
/// <param name="count">Page range count</param>
/// <param name="preFlush">True if the data should come from the flush buffer</param>
private void FlushPageRange(ulong address, ulong size, int startPage, int count, bool preFlush)
{
(int pageOffset, int pageSize) = GetOffset(startPage, count);
int offset = (int)(address - _address);
int end = offset + (int)size;
offset = Math.Max(offset, pageOffset);
end = Math.Min(end, pageOffset + pageSize);
if (end >= offset)
{
BufferHandle handle = preFlush ? _flushBuffer : _buffer.Handle;
_flushAction(handle, _address + (ulong)offset, (ulong)(end - offset));
}
}
/// <summary>
/// Flush the given address range back into guest memory, optionally using data from the flush buffer.
/// When a copy has been performed on or before the waited sync number, the data can come from the flush buffer.
/// Otherwise, it flushes the parent buffer directly.
/// </summary>
/// <param name="address">Range address</param>
/// <param name="size">Range size</param>
/// <param name="syncNumber">Sync number that has been waited for</param>
public void FlushWithAction(ulong address, ulong size, ulong syncNumber)
{
// Copy the parts of the range that have pre-flush copies that have been completed.
// Run the flush action for ranges that don't have pre-flush copies.
// If a range doesn't have a pre-flush copy, consider adding one.
(int baseIndex, int count) = GetPageRange(address, size);
bool rangePreFlushed = false;
int startPage = -1;
for (int i = 0; i < count; i++)
{
int pageIndex = baseIndex + i;
ref PreFlushPage page = ref _pages[pageIndex];
bool flushPage = false;
page.CopyCount = 0;
if (page.State == PreFlushState.HasCopied)
{
if (syncNumber >= page.FirstActivatedSync)
{
// After the range is first activated, its data will always be copied to the preflush buffer on each sync.
flushPage = true;
}
}
else if (page.State == PreFlushState.None)
{
page.State = PreFlushState.HasFlushed;
ShouldCopy = true;
}
if (flushPage)
{
if (!rangePreFlushed || startPage == -1)
{
if (startPage != -1)
{
FlushPageRange(address, size, startPage, pageIndex - startPage, false);
}
rangePreFlushed = true;
startPage = pageIndex;
}
}
else if (rangePreFlushed || startPage == -1)
{
if (startPage != -1)
{
FlushPageRange(address, size, startPage, pageIndex - startPage, true);
}
rangePreFlushed = false;
startPage = pageIndex;
}
}
if (startPage != -1)
{
FlushPageRange(address, size, startPage, (baseIndex + count) - startPage, rangePreFlushed);
}
}
/// <summary>
/// Dispose the flush buffer, if present.
/// </summary>
public void Dispose()
{
if (_flushBuffer != BufferHandle.Null)
{
_context.Renderer.DeleteBuffer(_flushBuffer);
}
}
}
}

View file

@ -0,0 +1,99 @@
using Ryujinx.Graphics.Shader;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Gpu.Memory
{
/// <summary>
/// Pipeline stages that can modify buffer data, as well as flags indicating storage usage.
/// Must match ShaderStage for the shader stages, though anything after that can be in any order.
/// </summary>
internal enum BufferStage : byte
{
Compute,
Vertex,
TessellationControl,
TessellationEvaluation,
Geometry,
Fragment,
Indirect,
VertexBuffer,
IndexBuffer,
Copy,
TransformFeedback,
Internal,
None,
StageMask = 0x3f,
StorageMask = 0xc0,
StorageRead = 0x40,
StorageWrite = 0x80,
#pragma warning disable CA1069 // Enums values should not be duplicated
StorageAtomic = 0xc0
#pragma warning restore CA1069 // Enums values should not be duplicated
}
/// <summary>
/// Utility methods to convert shader stages and binding flags into buffer stages.
/// </summary>
internal static class BufferStageUtils
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStage FromShaderStage(ShaderStage stage)
{
return (BufferStage)stage;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStage FromShaderStage(int stageIndex)
{
return (BufferStage)(stageIndex + 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStage FromUsage(BufferUsageFlags flags)
{
if (flags.HasFlag(BufferUsageFlags.Write))
{
return BufferStage.StorageWrite;
}
else
{
return BufferStage.StorageRead;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStage FromUsage(TextureUsageFlags flags)
{
if (flags.HasFlag(TextureUsageFlags.ImageStore))
{
return BufferStage.StorageWrite;
}
else
{
return BufferStage.StorageRead;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStage TextureBuffer(ShaderStage shaderStage, TextureUsageFlags flags)
{
return FromShaderStage(shaderStage) | FromUsage(flags);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStage GraphicsStorage(int stageIndex, BufferUsageFlags flags)
{
return FromShaderStage(stageIndex) | FromUsage(flags);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStage ComputeStorage(BufferUsageFlags flags)
{
return BufferStage.Compute | FromUsage(flags);
}
}
}

View file

@ -58,15 +58,17 @@ namespace Ryujinx.Graphics.Gpu.Shader
TextureBindings[i] = stage.Info.Textures.Select(descriptor =>
{
Target target = ShaderTexture.GetTarget(descriptor.Type);
Target target = descriptor.Type != SamplerType.None ? ShaderTexture.GetTarget(descriptor.Type) : default;
var result = new TextureBindingInfo(
target,
descriptor.Set,
descriptor.Binding,
descriptor.ArrayLength,
descriptor.CbufSlot,
descriptor.HandleIndex,
descriptor.Flags);
descriptor.Flags,
descriptor.Type == SamplerType.None);
if (descriptor.ArrayLength <= 1)
{
@ -89,6 +91,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
var result = new TextureBindingInfo(
target,
format,
descriptor.Set,
descriptor.Binding,
descriptor.ArrayLength,
descriptor.CbufSlot,

View file

@ -125,9 +125,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
CompressionAlgorithm algorithm = CompressionAlgorithm.None;
Read(ref algorithm);
if (algorithm == CompressionAlgorithm.Deflate)
switch (algorithm)
{
case CompressionAlgorithm.None:
break;
case CompressionAlgorithm.Deflate:
_activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true);
break;
case CompressionAlgorithm.Brotli:
_activeStream = new BrotliStream(_stream, CompressionMode.Decompress, true);
break;
default:
throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\"");
}
}
@ -139,9 +148,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
Write(ref algorithm);
if (algorithm == CompressionAlgorithm.Deflate)
switch (algorithm)
{
_activeStream = new DeflateStream(_stream, CompressionLevel.SmallestSize, true);
case CompressionAlgorithm.None:
break;
case CompressionAlgorithm.Deflate:
_activeStream = new DeflateStream(_stream, CompressionLevel.Fastest, true);
break;
case CompressionAlgorithm.Brotli:
_activeStream = new BrotliStream(_stream, CompressionLevel.Fastest, true);
break;
default:
throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\"");
}
}
@ -177,7 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
switch (algorithm)
{
case CompressionAlgorithm.None:
stream.Read(data);
stream.ReadExactly(data);
break;
case CompressionAlgorithm.Deflate:
stream = new DeflateStream(stream, CompressionMode.Decompress, true);
@ -187,6 +205,14 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
}
stream.Dispose();
break;
case CompressionAlgorithm.Brotli:
stream = new BrotliStream(stream, CompressionMode.Decompress, true);
for (int offset = 0; offset < data.Length;)
{
offset += stream.Read(data[offset..]);
}
stream.Dispose();
break;
}
}
@ -206,7 +232,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
stream.Write(data);
break;
case CompressionAlgorithm.Deflate:
stream = new DeflateStream(stream, CompressionLevel.SmallestSize, true);
stream = new DeflateStream(stream, CompressionLevel.Fastest, true);
stream.Write(data);
stream.Dispose();
break;
case CompressionAlgorithm.Brotli:
stream = new BrotliStream(stream, CompressionLevel.Fastest, true);
stream.Write(data);
stream.Dispose();
break;

View file

@ -14,5 +14,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// Deflate compression (RFC 1951).
/// </summary>
Deflate,
/// <summary>
/// Brotli compression (RFC 7932).
/// </summary>
Brotli,
}
}

View file

@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// <returns>Compression algorithm</returns>
public static CompressionAlgorithm GetCompressionAlgorithm()
{
return CompressionAlgorithm.Deflate;
return CompressionAlgorithm.Brotli;
}
}
}

View file

@ -18,6 +18,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private readonly ShaderSpecializationState _newSpecState;
private readonly int _stageIndex;
private readonly bool _isVulkan;
private readonly bool _hasGeometryShader;
private readonly bool _supportsQuads;
/// <summary>
/// Creates a new instance of the cached GPU state accessor for shader translation.
@ -29,6 +31,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// <param name="newSpecState">Shader specialization state of the recompiled shader</param>
/// <param name="counts">Resource counts shared across all shader stages</param>
/// <param name="stageIndex">Shader stage index</param>
/// <param name="hasGeometryShader">Indicates if a geometry shader is present</param>
public DiskCacheGpuAccessor(
GpuContext context,
ReadOnlyMemory<byte> data,
@ -36,7 +39,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
ShaderSpecializationState oldSpecState,
ShaderSpecializationState newSpecState,
ResourceCounts counts,
int stageIndex) : base(context, counts, stageIndex)
int stageIndex,
bool hasGeometryShader) : base(context, counts, stageIndex)
{
_data = data;
_cb1Data = cb1Data;
@ -44,6 +48,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
_newSpecState = newSpecState;
_stageIndex = stageIndex;
_isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
_hasGeometryShader = hasGeometryShader;
_supportsQuads = context.Capabilities.SupportsQuads;
if (stageIndex == (int)ShaderStage.Geometry - 1)
{
@ -100,7 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// <inheritdoc/>
public GpuGraphicsState QueryGraphicsState()
{
return _oldSpecState.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled);
return _oldSpecState.GraphicsState.CreateShaderGraphicsState(
!_isVulkan,
_supportsQuads,
_hasGeometryShader,
_isVulkan || _oldSpecState.GraphicsState.YNegateEnabled);
}
/// <inheritdoc/>
@ -109,6 +119,13 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
return _oldSpecState.GraphicsState.HasConstantBufferDrawParameters;
}
/// <inheritdoc/>
/// <exception cref="DiskCacheLoadException">Pool length is not available on the cache</exception>
public int QuerySamplerArrayLengthFromPool()
{
return QueryArrayLengthFromPool(isSampler: true);
}
/// <inheritdoc/>
public SamplerType QuerySamplerType(int handle, int cbufSlot)
{
@ -117,6 +134,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
}
/// <inheritdoc/>
/// <exception cref="DiskCacheLoadException">Constant buffer derived length is not available on the cache</exception>
public int QueryTextureArrayLengthFromBuffer(int slot)
{
if (!_oldSpecState.TextureArrayFromBufferRegistered(_stageIndex, 0, slot))
@ -130,6 +148,13 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
return arrayLength;
}
/// <inheritdoc/>
/// <exception cref="DiskCacheLoadException">Pool length is not available on the cache</exception>
public int QueryTextureArrayLengthFromPool()
{
return QueryArrayLengthFromPool(isSampler: false);
}
/// <inheritdoc/>
public TextureFormat QueryTextureFormat(int handle, int cbufSlot)
{
@ -170,6 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
}
/// <inheritdoc/>
/// <exception cref="DiskCacheLoadException">Texture information is not available on the cache</exception>
public void RegisterTexture(int handle, int cbufSlot)
{
if (!_oldSpecState.TextureRegistered(_stageIndex, handle, cbufSlot))
@ -182,5 +208,24 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
bool coordNormalized = _oldSpecState.GetCoordNormalized(_stageIndex, handle, cbufSlot);
_newSpecState.RegisterTexture(_stageIndex, handle, cbufSlot, format, formatSrgb, target, coordNormalized);
}
/// <summary>
/// Gets the cached texture or sampler pool capacity.
/// </summary>
/// <param name="isSampler">True to get sampler pool length, false for texture pool length</param>
/// <returns>Pool length</returns>
/// <exception cref="DiskCacheLoadException">Pool length is not available on the cache</exception>
private int QueryArrayLengthFromPool(bool isSampler)
{
if (!_oldSpecState.TextureArrayFromPoolRegistered(isSampler))
{
throw new DiskCacheLoadException(DiskCacheLoadResult.MissingTextureArrayLength);
}
int arrayLength = _oldSpecState.GetTextureArrayFromPoolLength(isSampler);
_newSpecState.RegisterTextureArrayLengthFromPool(isSampler, arrayLength);
return arrayLength;
}
}
}

View file

@ -220,7 +220,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
}
dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
dataFileStream.Read(cb1Data);
dataFileStream.ReadExactly(cb1Data);
BinarySerializer.ReadCompressed(dataFileStream, guestCode);
_cache[index] = (guestCode, cb1Data);
@ -279,7 +279,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
byte[] cachedCode = new byte[entry.CodeSize];
byte[] cachedCb1Data = new byte[entry.Cb1DataSize];
dataFileStream.Read(cachedCb1Data);
dataFileStream.ReadExactly(cachedCb1Data);
BinarySerializer.ReadCompressed(dataFileStream, cachedCode);
if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data))

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 = 6489;
private const uint CodeGenVersion = 6852;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";

View file

@ -601,6 +601,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
TargetApi api = _context.Capabilities.Api;
bool hasCachedGs = guestShaders[4].HasValue;
for (int stageIndex = Constants.ShaderStages - 1; stageIndex >= 0; stageIndex--)
{
if (guestShaders[stageIndex + 1].HasValue)
@ -610,7 +612,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
byte[] guestCode = shader.Code;
byte[] cb1Data = shader.Cb1Data;
DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex);
DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex, hasCachedGs);
TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, 0);
if (nextStage != null)
@ -623,7 +625,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
byte[] guestCodeA = guestShaders[0].Value.Code;
byte[] cb1DataA = guestShaders[0].Value.Cb1Data;
DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0);
DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0, hasCachedGs);
translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, api, DefaultFlags | TranslationFlags.VertexA, 0);
}
@ -711,7 +713,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
GuestCodeAndCbData shader = guestShaders[0].Value;
ResourceCounts counts = new();
ShaderSpecializationState newSpecState = new(ref specState.ComputeState);
DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0);
DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0, false);
gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false);
TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, 0);

View file

@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly int _stageIndex;
private readonly bool _compute;
private readonly bool _isVulkan;
private readonly bool _hasGeometryShader;
private readonly bool _supportsQuads;
/// <summary>
/// Creates a new instance of the GPU state accessor for graphics shader translation.
@ -25,12 +27,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <param name="channel">GPU channel</param>
/// <param name="state">Current GPU state</param>
/// <param name="stageIndex">Graphics shader stage index (0 = Vertex, 4 = Fragment)</param>
public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state, int stageIndex) : base(context, state.ResourceCounts, stageIndex)
/// <param name="hasGeometryShader">Indicates if a geometry shader is present</param>
public GpuAccessor(
GpuContext context,
GpuChannel channel,
GpuAccessorState state,
int stageIndex,
bool hasGeometryShader) : base(context, state.ResourceCounts, stageIndex)
{
_isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
_channel = channel;
_state = state;
_stageIndex = stageIndex;
_isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
_hasGeometryShader = hasGeometryShader;
_supportsQuads = context.Capabilities.SupportsQuads;
if (stageIndex == (int)ShaderStage.Geometry - 1)
{
@ -105,7 +115,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// <inheritdoc/>
public GpuGraphicsState QueryGraphicsState()
{
return _state.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _state.GraphicsState.YNegateEnabled);
return _state.GraphicsState.CreateShaderGraphicsState(
!_isVulkan,
_supportsQuads,
_hasGeometryShader,
_isVulkan || _state.GraphicsState.YNegateEnabled);
}
/// <inheritdoc/>
@ -120,6 +134,15 @@ namespace Ryujinx.Graphics.Gpu.Shader
return _state.GraphicsState.HasUnalignedStorageBuffer || _state.ComputeState.HasUnalignedStorageBuffer;
}
/// <inheritdoc/>
public int QuerySamplerArrayLengthFromPool()
{
int length = _state.SamplerPoolMaximumId + 1;
_state.SpecializationState?.RegisterTextureArrayLengthFromPool(isSampler: true, length);
return length;
}
/// <inheritdoc/>
public SamplerType QuerySamplerType(int handle, int cbufSlot)
{
@ -141,6 +164,15 @@ namespace Ryujinx.Graphics.Gpu.Shader
return arrayLength;
}
/// <inheritdoc/>
public int QueryTextureArrayLengthFromPool()
{
int length = _state.PoolState.TexturePoolMaximumId + 1;
_state.SpecializationState?.RegisterTextureArrayLengthFromPool(isSampler: false, length);
return length;
}
//// <inheritdoc/>
public TextureFormat QueryTextureFormat(int handle, int cbufSlot)
{

View file

@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
_reservedImages = rrc.ReservedImages;
}
public int CreateConstantBufferBinding(int index)
public SetBindingPair CreateConstantBufferBinding(int index)
{
int binding;
@ -64,10 +64,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
binding = _resourceCounts.UniformBuffersCount++;
}
return binding + _reservedConstantBuffers;
return new SetBindingPair(_context.Capabilities.UniformBufferSetIndex, binding + _reservedConstantBuffers);
}
public int CreateImageBinding(int count, bool isBuffer)
public SetBindingPair CreateImageBinding(int count, bool isBuffer)
{
int binding;
@ -96,10 +96,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
_resourceCounts.ImagesCount += count;
}
return binding + _reservedImages;
return new SetBindingPair(_context.Capabilities.ImageSetIndex, binding + _reservedImages);
}
public int CreateStorageBufferBinding(int index)
public SetBindingPair CreateStorageBufferBinding(int index)
{
int binding;
@ -112,10 +112,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
binding = _resourceCounts.StorageBuffersCount++;
}
return binding + _reservedStorageBuffers;
return new SetBindingPair(_context.Capabilities.StorageBufferSetIndex, binding + _reservedStorageBuffers);
}
public int CreateTextureBinding(int count, bool isBuffer)
public SetBindingPair CreateTextureBinding(int count, bool isBuffer)
{
int binding;
@ -144,7 +144,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
_resourceCounts.TexturesCount += count;
}
return binding + _reservedTextures;
return new SetBindingPair(_context.Capabilities.TextureSetIndex, binding + _reservedTextures);
}
private int GetBindingFromIndex(int index, uint maxPerStage, string resourceName)
@ -183,6 +183,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
return maxPerStage * Constants.ShaderStages;
}
public int CreateExtraSet()
{
if (_resourceCounts.SetsCount >= _context.Capabilities.MaximumExtraSets)
{
return -1;
}
return _context.Capabilities.ExtraSetBaseIndex + _resourceCounts.SetsCount++;
}
public int QueryHostGatherBiasPrecision() => _context.Capabilities.GatherBiasPrecision;
public bool QueryHostReducedPrecision() => _context.Capabilities.ReduceShaderPrecision;
@ -213,6 +223,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
public bool QueryHostSupportsScaledVertexFormats() => _context.Capabilities.SupportsScaledVertexFormats;
public bool QueryHostSupportsSeparateSampler() => _context.Capabilities.SupportsSeparateSampler;
public bool QueryHostSupportsShaderBallot() => _context.Capabilities.SupportsShaderBallot;
public bool QueryHostSupportsShaderBarrierDivergence() => _context.Capabilities.SupportsShaderBarrierDivergence;

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