Compare commits

..

14 Commits

Author SHA1 Message Date
8a352df3c6 Allow BGRA images on Vulkan (#5203) 2023-06-03 03:43:00 +00:00
c545c59851 ava: Fix exit dialog while guest is running. (#5207)
* ava: Fix exit dialog while guest is running.

There is currently an issue while a game runs, the content dialog creation method check if `IsGameRunning` is true to show the popup.
But the condition here is wrong (`window` is null) so it throw a NullException silently in `Dispatcher.UIThread`.
This is now fixed by using the right casting.

* improve condition

* Fix spacing
2023-06-03 03:37:00 +00:00
96ea4e8c8e nuget: bump Microsoft.NET.Test.Sdk from 17.6.0 to 17.6.1 (#5192)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.6.0 to 17.6.1.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.6.0...v17.6.1)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 21:03:00 +02:00
b8f48bcf64 UI: Fix empty homebrew icon (#5189)
* UI: Fix empty homebrew icon

We currently don't check the icon size when we read it from the homebrew data. That could cause issues at UI side since the buffer isn't null but empty. Extra check have been added UI side too.
(I cleaned up some files during my research too)

Fixes #5188

* Remove additional check

* Remove unused using
2023-06-01 18:24:00 +02:00
6966211e07 Give Library header DockPanel explicit height (#5160) 2023-06-01 17:40:44 +02:00
57524a4c8a Add issue template for missing shader instructions (#5048)
* Add issue template for missing shader instructions

* fixup! Add issue template for missing shader instructions

* Update .github/ISSUE_TEMPLATE/missing_shader_instruction.yml

---------

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2023-06-01 17:32:38 +02:00
f4539c49d8 [Logger] Add print with stacktrace method (#5129)
* Add print with stacktrace method

* Adjust logging namespaces

* Add static keyword to DynamicObjectFormatter
2023-06-01 13:47:53 +00:00
12c62fdbc2 nuget: bump DynamicData from 7.13.8 to 7.14.2 (#5148)
Bumps [DynamicData](https://github.com/reactiveui/DynamicData) from 7.13.8 to 7.14.2.
- [Release notes](https://github.com/reactiveui/DynamicData/releases)
- [Changelog](https://github.com/reactivemarbles/DynamicData/blob/main/ReleaseNotes.md)
- [Commits](https://github.com/reactiveui/DynamicData/compare/7.13.8...7.14.2)

---
updated-dependencies:
- dependency-name: DynamicData
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 13:35:04 +00:00
e3c6be5e29 Only run one workflow for a PR at a time (#5137) 2023-06-01 09:42:49 +02:00
4741a05df9 Vulkan: Include DepthMode in ProgramPipelineState (#5185) 2023-06-01 09:05:39 +02:00
c6676007bf GPU: Dispose Renderer after running deferred actions (#5144)
* GAL: Dispose Renderer after running deferred actions

Deferred actions from disposing physical memory instances always dispose the resources in their caches. The renderer can't be disposed before these resources get disposed, otherwise the dispose actions will not actually run, and the ThreadedRenderer may get stuck trying to enqueue too many commands when there is nothing consuming them.

This should fix most instances of the emulator freezing on close.

* Wait for main render commands to finish, but keep RenderThread alive til dispose

* Address some feedback.

* No parameterize needed

* Set thread name as part of constructor

* Port to Ava and SDL2
2023-05-31 21:43:20 +00:00
92b0b7d753 Avalonia UI: Fix letter "x" in Ryujinx logo being cut off (#5176)
Also make the pronunciation center-aligned
2023-05-31 21:03:11 +00:00
232237bf28 Skip draws with zero vertex count (#5149) 2023-05-31 17:51:11 -03:00
c27e453fd3 Share ResourceManager vertex vertex A and B shaders (#5181) 2023-05-31 17:17:50 -03:00
42 changed files with 229 additions and 150 deletions

View File

@ -0,0 +1,19 @@
name: Missing Shader Instruction
description: Shader Instruction is missing in Ryujinx.
title: "[GPU]"
labels: [gpu, not-implemented]
body:
- type: textarea
id: instruction
attributes:
label: Shader instruction
description: What shader instruction is missing?
validations:
required: true
- type: textarea
id: required
attributes:
label: Required by
description: Add links to the [compatibility list page(s)](https://github.com/Ryujinx/Ryujinx-Games-List/issues) of the game(s) that require this instruction.
validations:
required: true

View File

@ -18,6 +18,10 @@ on:
- '*.yml' - '*.yml'
- 'README.md' - 'README.md'
concurrency:
group: pr-checks-${{ github.event.number }}
cancel-in-progress: true
env: env:
POWERSHELL_TELEMETRY_OPTOUT: 1 POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1

View File

@ -13,7 +13,7 @@
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Concentus" Version="1.1.7" /> <PackageVersion Include="Concentus" Version="1.1.7" />
<PackageVersion Include="DiscordRichPresence" Version="1.1.3.18" /> <PackageVersion Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageVersion Include="DynamicData" Version="7.13.8" /> <PackageVersion Include="DynamicData" Version="7.14.2" />
<PackageVersion Include="FluentAvaloniaUI" Version="1.4.5" /> <PackageVersion Include="FluentAvaloniaUI" Version="1.4.5" />
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" /> <PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" /> <PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
@ -21,7 +21,7 @@
<PackageVersion Include="LibHac" Version="0.18.0" /> <PackageVersion Include="LibHac" Version="0.18.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.1" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" /> <PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" /> <PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
<PackageVersion Include="NUnit" Version="3.13.3" /> <PackageVersion Include="NUnit" Version="3.13.3" />

View File

@ -92,6 +92,8 @@ namespace Ryujinx.Ava
private bool _isActive; private bool _isActive;
private bool _renderingStarted; private bool _renderingStarted;
private ManualResetEvent _gpuDoneEvent;
private IRenderer _renderer; private IRenderer _renderer;
private readonly Thread _renderingThread; private readonly Thread _renderingThread;
private readonly CancellationTokenSource _gpuCancellationTokenSource; private readonly CancellationTokenSource _gpuCancellationTokenSource;
@ -183,6 +185,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
_gpuDoneEvent = new ManualResetEvent(false);
} }
private void TopLevel_PointerEnterOrMoved(object sender, PointerEventArgs e) private void TopLevel_PointerEnterOrMoved(object sender, PointerEventArgs e)
@ -423,10 +426,10 @@ namespace Ryujinx.Ava
_isActive = false; _isActive = false;
if (_renderingThread.IsAlive) // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
{ // We only need to wait for all commands submitted during the main gpu loop to be processed.
_renderingThread.Join(); _gpuDoneEvent.WaitOne();
} _gpuDoneEvent.Dispose();
DisplaySleep.Restore(); DisplaySleep.Restore();
@ -917,6 +920,14 @@ namespace Ryujinx.Ava
UpdateStatus(); UpdateStatus();
} }
} }
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
}); });
(_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null); (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);

View File

@ -21,6 +21,7 @@ namespace Ryujinx.Ava.UI.Helpers
if (value is byte[] buffer && targetType == typeof(IImage)) if (value is byte[] buffer && targetType == typeof(IImage))
{ {
MemoryStream mem = new(buffer); MemoryStream mem = new(buffer);
return new Bitmap(mem); return new Bitmap(mem);
} }

View File

@ -318,7 +318,7 @@ namespace Ryujinx.Ava.UI.Helpers
Window parent = GetMainWindow(); Window parent = GetMainWindow();
if (parent is { IsActive: true } and MainWindow window && window.ViewModel.IsGameRunning) if (parent != null && parent.IsActive && (parent as MainWindow).ViewModel.IsGameRunning)
{ {
contentDialogOverlayWindow = new() contentDialogOverlayWindow = new()
{ {

View File

@ -9,9 +9,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common; using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;

View File

@ -16,6 +16,7 @@
</Design.DataContext> </Design.DataContext>
<DockPanel <DockPanel
Margin="0,0,0,5" Margin="0,0,0,5"
Height="35"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<Button <Button
Width="40" Width="40"

View File

@ -58,11 +58,20 @@
JustifyContent="SpaceAround" JustifyContent="SpaceAround"
RowSpacing="2"> RowSpacing="2">
<TextBlock <TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="28" FontSize="28"
FontWeight="Bold" FontWeight="Bold"
Text="Ryujinx" Text="Ryujinx"
TextAlignment="Left" /> TextAlignment="Center"
<TextBlock Text="(REE-YOU-JINX)" TextAlignment="Left" /> Width="100" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
Text="(REE-YOU-JINX)"
TextAlignment="Center"
Width="100" />
</flex:FlexPanel> </flex:FlexPanel>
</Grid> </Grid>
<TextBlock <TextBlock

View File

@ -519,14 +519,14 @@ namespace Ryujinx.Ava.UI.Windows
private void ConfirmExit() private void ConfirmExit()
{ {
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread.InvokeAsync(async () =>
{ {
ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog(); ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog();
if (ViewModel.IsClosing) if (ViewModel.IsClosing)
{ {
Close(); Close();
} }
}); });
} }
public async void LoadApplications() public async void LoadApplications()

View File

@ -1,6 +1,7 @@
using System.Text; using System.Diagnostics;
using System.Text;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Formatters
{ {
internal class DefaultLogFormatter : ILogFormatter internal class DefaultLogFormatter : ILogFormatter
{ {
@ -27,6 +28,14 @@ namespace Ryujinx.Common.Logging
if (args.Data is not null) if (args.Data is not null)
{ {
if (args.Data is StackTrace trace)
{
sb.Append('\n');
sb.Append(trace);
return sb.ToString();
}
sb.Append(' '); sb.Append(' ');
DynamicObjectFormatter.Format(sb, args.Data); DynamicObjectFormatter.Format(sb, args.Data);
} }
@ -39,4 +48,4 @@ namespace Ryujinx.Common.Logging
} }
} }
} }
} }

View File

@ -3,9 +3,9 @@ using System;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Formatters
{ {
internal class DynamicObjectFormatter internal static class DynamicObjectFormatter
{ {
private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>(); private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
@ -17,7 +17,7 @@ namespace Ryujinx.Common.Logging
} }
StringBuilder sb = StringBuilderPool.Allocate(); StringBuilder sb = StringBuilderPool.Allocate();
try try
{ {
Format(sb, dynamicObject); Format(sb, dynamicObject);

View File

@ -1,7 +1,7 @@
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Formatters
{ {
interface ILogFormatter interface ILogFormatter
{ {
string Format(LogEventArgs args); string Format(LogEventArgs args);
} }
} }

View File

@ -1,4 +1,5 @@
using System; using Ryujinx.Common.Logging.Formatters;
using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging

View File

@ -1,3 +1,4 @@
using Ryujinx.Common.Logging.Targets;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -55,6 +56,16 @@ namespace Ryujinx.Common.Logging
} }
} }
[StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PrintStack(LogClass logClass, string message, [CallerMemberName] string caller = "")
{
if (m_EnabledClasses[(int)logClass])
{
Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message), new StackTrace(true)));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PrintStub(LogClass logClass, string message = "", [CallerMemberName] string caller = "") public void PrintStub(LogClass logClass, string message = "", [CallerMemberName] string caller = "")
{ {
@ -122,7 +133,7 @@ namespace Ryujinx.Common.Logging
AsyncLogTargetOverflowAction.Discard)); AsyncLogTargetOverflowAction.Discard));
Notice = new Log(LogLevel.Notice); Notice = new Log(LogLevel.Notice);
// Enable important log levels before configuration is loaded // Enable important log levels before configuration is loaded
Error = new Log(LogLevel.Error); Error = new Log(LogLevel.Error);
Warning = new Log(LogLevel.Warning); Warning = new Log(LogLevel.Warning);
@ -221,4 +232,4 @@ namespace Ryujinx.Common.Logging
m_EnabledClasses[(int)logClass] = enabled; m_EnabledClasses[(int)logClass] = enabled;
} }
} }
} }

View File

@ -2,7 +2,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading; using System.Threading;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Targets
{ {
public enum AsyncLogTargetOverflowAction public enum AsyncLogTargetOverflowAction
{ {

View File

@ -1,6 +1,7 @@
using System; using Ryujinx.Common.Logging.Formatters;
using System;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Targets
{ {
public class ConsoleLogTarget : ILogTarget public class ConsoleLogTarget : ILogTarget
{ {
@ -38,4 +39,4 @@ namespace Ryujinx.Common.Logging
Console.ResetColor(); Console.ResetColor();
} }
} }
} }

View File

@ -1,8 +1,9 @@
using System; using Ryujinx.Common.Logging.Formatters;
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Targets
{ {
public class FileLogTarget : ILogTarget public class FileLogTarget : ILogTarget
{ {

View File

@ -1,6 +1,6 @@
using System; using System;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Targets
{ {
public interface ILogTarget : IDisposable public interface ILogTarget : IDisposable
{ {

View File

@ -1,7 +1,7 @@
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using System.IO; using System.IO;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging.Targets
{ {
public class JsonLogTarget : ILogTarget public class JsonLogTarget : ILogTarget
{ {

View File

@ -383,6 +383,7 @@ namespace Ryujinx.Graphics.GAL
case Format.R10G10B10A2Unorm: case Format.R10G10B10A2Unorm:
case Format.R10G10B10A2Uint: case Format.R10G10B10A2Uint:
case Format.R11G11B10Float: case Format.R11G11B10Float:
case Format.B8G8R8A8Unorm:
return true; return true;
} }

View File

@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using System; using System;
using System.Threading;
namespace Ryujinx.Graphics.GAL namespace Ryujinx.Graphics.GAL
{ {
@ -52,7 +53,7 @@ namespace Ryujinx.Graphics.GAL
void ResetCounter(CounterType type); void ResetCounter(CounterType type);
void RunLoop(Action gpuLoop) void RunLoop(ThreadStart gpuLoop)
{ {
gpuLoop(); gpuLoop();
} }

View File

@ -30,7 +30,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
private IRenderer _baseRenderer; private IRenderer _baseRenderer;
private Thread _gpuThread; private Thread _gpuThread;
private Thread _backendThread; private Thread _backendThread;
private bool _disposed;
private bool _running; private bool _running;
private AutoResetEvent _frameComplete = new AutoResetEvent(true); private AutoResetEvent _frameComplete = new AutoResetEvent(true);
@ -98,19 +97,17 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_refQueue = new object[MaxRefsPerCommand * QueueCount]; _refQueue = new object[MaxRefsPerCommand * QueueCount];
} }
public void RunLoop(Action gpuLoop) public void RunLoop(ThreadStart gpuLoop)
{ {
_running = true; _running = true;
_backendThread = Thread.CurrentThread; _backendThread = Thread.CurrentThread;
_gpuThread = new Thread(() => { _gpuThread = new Thread(gpuLoop)
gpuLoop(); {
_running = false; Name = "GPU.MainThread"
_galWorkAvailable.Set(); };
});
_gpuThread.Name = "GPU.MainThread";
_gpuThread.Start(); _gpuThread.Start();
RenderLoop(); RenderLoop();
@ -120,7 +117,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
{ {
// Power through the render queue until the Gpu thread work is done. // Power through the render queue until the Gpu thread work is done.
while (_running && !_disposed) while (_running)
{ {
_galWorkAvailable.Wait(); _galWorkAvailable.Wait();
_galWorkAvailable.Reset(); _galWorkAvailable.Reset();
@ -488,12 +485,23 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return _baseRenderer.PrepareHostMapping(address, size); return _baseRenderer.PrepareHostMapping(address, size);
} }
public void FlushThreadedCommands()
{
SpinWait wait = new();
while (Volatile.Read(ref _commandCount) > 0)
{
wait.SpinOnce();
}
}
public void Dispose() public void Dispose()
{ {
// Dispose must happen from the render thread, after all commands have completed. // Dispose must happen from the render thread, after all commands have completed.
// Stop the GPU thread. // Stop the GPU thread.
_disposed = true; _running = false;
_galWorkAvailable.Set();
if (_gpuThread != null && _gpuThread.IsAlive) if (_gpuThread != null && _gpuThread.IsAlive)
{ {

View File

@ -63,6 +63,8 @@ namespace Ryujinx.Graphics.GAL
public bool PrimitiveRestartEnable; public bool PrimitiveRestartEnable;
public uint PatchControlPoints; public uint PatchControlPoints;
public DepthMode DepthMode;
public void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs) public void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs)
{ {
VertexAttribCount = vertexAttribs.Length; VertexAttribCount = vertexAttribs.Length;

View File

@ -771,7 +771,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary> /// </summary>
private void UpdateDepthMode() private void UpdateDepthMode()
{ {
_context.Renderer.Pipeline.SetDepthMode(GetDepthMode()); DepthMode mode = GetDepthMode();
_pipeline.DepthMode = mode;
_context.Renderer.Pipeline.SetDepthMode(mode);
} }
/// <summary> /// <summary>

View File

@ -390,7 +390,6 @@ namespace Ryujinx.Graphics.Gpu
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
Renderer.Dispose();
GPFifo.Dispose(); GPFifo.Dispose();
HostInitalized.Dispose(); HostInitalized.Dispose();
@ -403,6 +402,8 @@ namespace Ryujinx.Graphics.Gpu
PhysicalMemoryRegistry.Clear(); PhysicalMemoryRegistry.Clear();
RunDeferredActions(); RunDeferredActions();
Renderer.Dispose();
} }
} }
} }

View File

@ -17,8 +17,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly ResourceCounts _resourceCounts; private readonly ResourceCounts _resourceCounts;
private readonly int _stageIndex; private readonly int _stageIndex;
private readonly int[] _constantBufferBindings;
/// <summary> /// <summary>
/// Creates a new GPU accessor. /// Creates a new GPU accessor.
/// </summary> /// </summary>
@ -28,12 +26,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
_context = context; _context = context;
_resourceCounts = resourceCounts; _resourceCounts = resourceCounts;
_stageIndex = stageIndex; _stageIndex = stageIndex;
if (context.Capabilities.Api != TargetApi.Vulkan)
{
_constantBufferBindings = new int[Constants.TotalGpUniformBuffers];
_constantBufferBindings.AsSpan().Fill(-1);
}
} }
public int QueryBindingConstantBuffer(int index) public int QueryBindingConstantBuffer(int index)
@ -45,15 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
} }
else else
{ {
int binding = _constantBufferBindings[index]; return _resourceCounts.UniformBuffersCount++;
if (binding < 0)
{
binding = _resourceCounts.UniformBuffersCount++;
_constantBufferBindings[index] = binding;
}
return binding;
} }
} }

View File

@ -736,6 +736,19 @@ namespace Ryujinx.Graphics.Gpu.Shader
return MatchesTexture(specializationState, descriptor); return MatchesTexture(specializationState, descriptor);
} }
/// <summary>
/// Populates pipeline state that doesn't exist in older caches with default values
/// based on specialization state.
/// </summary>
/// <param name="pipelineState">Pipeline state to prepare</param>
private void PreparePipelineState(ref ProgramPipelineState pipelineState)
{
if (!_compute)
{
pipelineState.DepthMode = GraphicsState.DepthMode ? DepthMode.MinusOneToOne : DepthMode.ZeroToOne;
}
}
/// <summary> /// <summary>
/// Reads shader specialization state that has been serialized. /// Reads shader specialization state that has been serialized.
/// </summary> /// </summary>
@ -776,6 +789,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
{ {
ProgramPipelineState pipelineState = default; ProgramPipelineState pipelineState = default;
dataReader.ReadWithMagicAndSize(ref pipelineState, PgpsMagic); dataReader.ReadWithMagicAndSize(ref pipelineState, PgpsMagic);
specState.PreparePipelineState(ref pipelineState);
specState.PipelineState = pipelineState; specState.PipelineState = pipelineState;
} }

View File

@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Shader.Translation
private readonly HashSet<int> _usedConstantBufferBindings; private readonly HashSet<int> _usedConstantBufferBindings;
public ShaderProperties Properties => _properties;
public ResourceManager(ShaderStage stage, IGpuAccessor gpuAccessor, ShaderProperties properties) public ResourceManager(ShaderStage stage, IGpuAccessor gpuAccessor, ShaderProperties properties)
{ {
_gpuAccessor = gpuAccessor; _gpuAccessor = gpuAccessor;
@ -98,19 +100,6 @@ namespace Ryujinx.Graphics.Shader.Translation
_properties.AddConstantBuffer(binding, new BufferDefinition(BufferLayout.Std140, 0, binding, name, type)); _properties.AddConstantBuffer(binding, new BufferDefinition(BufferLayout.Std140, 0, binding, name, type));
} }
public void InheritFrom(ResourceManager other)
{
for (int i = 0; i < other._cbSlotToBindingMap.Length; i++)
{
int binding = other._cbSlotToBindingMap[i];
if (binding >= 0)
{
_cbSlotToBindingMap[i] = binding;
}
}
}
public static string GetShaderStagePrefix(ShaderStage stage) public static string GetShaderStagePrefix(ShaderStage stage)
{ {
uint index = (uint)stage; uint index = (uint)stage;

View File

@ -39,9 +39,9 @@ namespace Ryujinx.Graphics.Shader.Translation
public TranslationOptions Options { get; } public TranslationOptions Options { get; }
public ShaderProperties Properties { get; } public ShaderProperties Properties => ResourceManager.Properties;
public ResourceManager ResourceManager { get; } public ResourceManager ResourceManager { get; set; }
public bool TransformFeedbackEnabled { get; } public bool TransformFeedbackEnabled { get; }
@ -159,8 +159,7 @@ namespace Ryujinx.Graphics.Shader.Translation
_sbSlots = new Dictionary<int, int>(); _sbSlots = new Dictionary<int, int>();
_sbSlotsReverse = new Dictionary<int, int>(); _sbSlotsReverse = new Dictionary<int, int>();
Properties = new ShaderProperties(); ResourceManager = new ResourceManager(stage, gpuAccessor, new ShaderProperties());
ResourceManager = new ResourceManager(stage, gpuAccessor, Properties);
} }
public ShaderConfig( public ShaderConfig(
@ -429,8 +428,6 @@ namespace Ryujinx.Graphics.Shader.Translation
public void InheritFrom(ShaderConfig other) public void InheritFrom(ShaderConfig other)
{ {
ResourceManager.InheritFrom(other.ResourceManager);
ClipDistancesWritten |= other.ClipDistancesWritten; ClipDistancesWritten |= other.ClipDistancesWritten;
UsedFeatures |= other.UsedFeatures; UsedFeatures |= other.UsedFeatures;

View File

@ -155,6 +155,9 @@ namespace Ryujinx.Graphics.Shader.Translation
{ {
other._config.MergeOutputUserAttributes(_config.UsedOutputAttributes, Enumerable.Empty<int>()); other._config.MergeOutputUserAttributes(_config.UsedOutputAttributes, Enumerable.Empty<int>());
// We need to share the resource manager since both shaders accesses the same constant buffers.
other._config.ResourceManager = _config.ResourceManager;
FunctionCode[] otherCode = EmitShader(other._program, other._config, initializeOutputs: true, out int aStart); FunctionCode[] otherCode = EmitShader(other._program, other._config, initializeOutputs: true, out int aStart);
code = Combine(otherCode, code, aStart); code = Combine(otherCode, code, aStart);

View File

@ -96,8 +96,6 @@ namespace Ryujinx.Graphics.Vulkan.Effects
{ {
var originalInfo = view.Info; var originalInfo = view.Info;
var swapRB = originalInfo.Format.IsBgr() && originalInfo.SwizzleR == SwizzleComponent.Red;
var info = new TextureCreateInfo( var info = new TextureCreateInfo(
width, width,
height, height,
@ -110,9 +108,9 @@ namespace Ryujinx.Graphics.Vulkan.Effects
originalInfo.Format, originalInfo.Format,
originalInfo.DepthStencilMode, originalInfo.DepthStencilMode,
originalInfo.Target, originalInfo.Target,
swapRB ? originalInfo.SwizzleB : originalInfo.SwizzleR, originalInfo.SwizzleR,
originalInfo.SwizzleG, originalInfo.SwizzleG,
swapRB ? originalInfo.SwizzleR : originalInfo.SwizzleB, originalInfo.SwizzleB,
originalInfo.SwizzleA); originalInfo.SwizzleA);
_intermediaryTexture?.Dispose(); _intermediaryTexture?.Dispose();
_intermediaryTexture = _renderer.CreateTexture(info, view.ScaleFactor) as TextureView; _intermediaryTexture = _renderer.CreateTexture(info, view.ScaleFactor) as TextureView;
@ -155,7 +153,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize);
_pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) });
_pipeline.SetImage(0, _intermediaryTexture, GAL.Format.R8G8B8A8Unorm); _pipeline.SetImage(0, _intermediaryTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format));
_pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.DispatchCompute(dispatchX, dispatchY, 1);
_pipeline.ComputeBarrier(); _pipeline.ComputeBarrier();

View File

@ -56,28 +56,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
if (_texture == null || _texture.Width != view.Width || _texture.Height != view.Height) if (_texture == null || _texture.Width != view.Width || _texture.Height != view.Height)
{ {
_texture?.Dispose(); _texture?.Dispose();
_texture = _renderer.CreateTexture(view.Info, view.ScaleFactor) as TextureView;
var info = view.Info;
if (view.Info.Format.IsBgr())
{
info = new TextureCreateInfo(info.Width,
info.Height,
info.Depth,
info.Levels,
info.Samples,
info.BlockWidth,
info.BlockHeight,
info.BytesPerPixel,
info.Format,
info.DepthStencilMode,
info.Target,
info.SwizzleB,
info.SwizzleG,
info.SwizzleR,
info.SwizzleA);
}
_texture = _renderer.CreateTexture(info, view.ScaleFactor) as TextureView;
} }
_pipeline.SetCommandBuffer(cbs); _pipeline.SetCommandBuffer(cbs);
@ -96,7 +75,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
var dispatchX = BitUtils.DivRoundUp(view.Width, IPostProcessingEffect.LocalGroupSize); var dispatchX = BitUtils.DivRoundUp(view.Width, IPostProcessingEffect.LocalGroupSize);
var dispatchY = BitUtils.DivRoundUp(view.Height, IPostProcessingEffect.LocalGroupSize); var dispatchY = BitUtils.DivRoundUp(view.Height, IPostProcessingEffect.LocalGroupSize);
_pipeline.SetImage(0, _texture, GAL.Format.R8G8B8A8Unorm); _pipeline.SetImage(0, _texture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format));
_pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.DispatchCompute(dispatchX, dispatchY, 1);
_renderer.BufferManager.Delete(bufferHandle); _renderer.BufferManager.Delete(bufferHandle);

View File

@ -4,7 +4,6 @@ using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation; using Ryujinx.Graphics.Shader.Translation;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using System; using System;
using Format = Ryujinx.Graphics.GAL.Format;
namespace Ryujinx.Graphics.Vulkan.Effects namespace Ryujinx.Graphics.Vulkan.Effects
{ {
@ -149,7 +148,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
1, 1,
1, 1,
1, 1,
Format.R8G8Unorm, GAL.Format.R8G8Unorm,
DepthStencilMode.Depth, DepthStencilMode.Depth,
Target.Texture2D, Target.Texture2D,
SwizzleComponent.Red, SwizzleComponent.Red,
@ -165,7 +164,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
1, 1,
1, 1,
1, 1,
Format.R8Unorm, GAL.Format.R8Unorm,
DepthStencilMode.Depth, DepthStencilMode.Depth,
Target.Texture2D, Target.Texture2D,
SwizzleComponent.Red, SwizzleComponent.Red,
@ -192,30 +191,9 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_edgeOutputTexture?.Dispose(); _edgeOutputTexture?.Dispose();
_blendOutputTexture?.Dispose(); _blendOutputTexture?.Dispose();
var info = view.Info; _outputTexture = _renderer.CreateTexture(view.Info, view.ScaleFactor) as TextureView;
_edgeOutputTexture = _renderer.CreateTexture(view.Info, view.ScaleFactor) as TextureView;
if (view.Info.Format.IsBgr()) _blendOutputTexture = _renderer.CreateTexture(view.Info, view.ScaleFactor) as TextureView;
{
info = new TextureCreateInfo(info.Width,
info.Height,
info.Depth,
info.Levels,
info.Samples,
info.BlockWidth,
info.BlockHeight,
info.BytesPerPixel,
info.Format,
info.DepthStencilMode,
info.Target,
info.SwizzleB,
info.SwizzleG,
info.SwizzleR,
info.SwizzleA);
}
_outputTexture = _renderer.CreateTexture(info, view.ScaleFactor) as TextureView;
_edgeOutputTexture = _renderer.CreateTexture(info, view.ScaleFactor) as TextureView;
_blendOutputTexture = _renderer.CreateTexture(info, view.ScaleFactor) as TextureView;
} }
_pipeline.SetCommandBuffer(cbs); _pipeline.SetCommandBuffer(cbs);
@ -240,7 +218,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_renderer.BufferManager.SetData(bufferHandle, 0, resolutionBuffer); _renderer.BufferManager.SetData(bufferHandle, 0, resolutionBuffer);
var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize);
_pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) });
_pipeline.SetImage(0, _edgeOutputTexture, GAL.Format.R8G8B8A8Unorm); _pipeline.SetImage(0, _edgeOutputTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format));
_pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.DispatchCompute(dispatchX, dispatchY, 1);
_pipeline.ComputeBarrier(); _pipeline.ComputeBarrier();
@ -250,7 +228,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, _edgeOutputTexture, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, _edgeOutputTexture, _samplerLinear);
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _areaTexture, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _areaTexture, _samplerLinear);
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 4, _searchTexture, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 4, _searchTexture, _samplerLinear);
_pipeline.SetImage(0, _blendOutputTexture, GAL.Format.R8G8B8A8Unorm); _pipeline.SetImage(0, _blendOutputTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format));
_pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.DispatchCompute(dispatchX, dispatchY, 1);
_pipeline.ComputeBarrier(); _pipeline.ComputeBarrier();
@ -259,7 +237,7 @@ namespace Ryujinx.Graphics.Vulkan.Effects
_pipeline.Specialize(_specConstants); _pipeline.Specialize(_specConstants);
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _blendOutputTexture, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 3, _blendOutputTexture, _samplerLinear);
_pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _samplerLinear); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _samplerLinear);
_pipeline.SetImage(0, _outputTexture, GAL.Format.R8G8B8A8Unorm); _pipeline.SetImage(0, _outputTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format));
_pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.DispatchCompute(dispatchX, dispatchY, 1);
_pipeline.ComputeBarrier(); _pipeline.ComputeBarrier();

View File

@ -169,6 +169,16 @@ namespace Ryujinx.Graphics.Vulkan
return _table[(int)format]; return _table[(int)format];
} }
public static Format ConvertRgba8SrgbToUnorm(Format format)
{
return format switch
{
Format.R8G8B8A8Srgb => Format.R8G8B8A8Unorm,
Format.B8G8R8A8Srgb => Format.B8G8R8A8Unorm,
_ => format
};
}
public static int GetAttributeFormatSize(VkFormat format) public static int GetAttributeFormatSize(VkFormat format)
{ {
switch (format) switch (format)

View File

@ -358,7 +358,7 @@ namespace Ryujinx.Graphics.Vulkan
public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance) public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
{ {
if (!_program.IsLinked) if (!_program.IsLinked || vertexCount == 0)
{ {
return; return;
} }
@ -422,7 +422,7 @@ namespace Ryujinx.Graphics.Vulkan
public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance) public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance)
{ {
if (!_program.IsLinked) if (!_program.IsLinked || indexCount == 0)
{ {
return; return;
} }

View File

@ -165,6 +165,7 @@ namespace Ryujinx.Graphics.Vulkan
pipeline.DepthTestEnable = state.DepthTest.TestEnable; pipeline.DepthTestEnable = state.DepthTest.TestEnable;
pipeline.DepthWriteEnable = state.DepthTest.WriteEnable; pipeline.DepthWriteEnable = state.DepthTest.WriteEnable;
pipeline.DepthCompareOp = state.DepthTest.Func.Convert(); pipeline.DepthCompareOp = state.DepthTest.Func.Convert();
pipeline.DepthMode = state.DepthMode == DepthMode.MinusOneToOne;
pipeline.FrontFace = state.FrontFace.Convert(); pipeline.FrontFace = state.FrontFace.Convert();

View File

@ -9,6 +9,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Targets;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Cpu; using Ryujinx.Cpu;

View File

@ -62,6 +62,7 @@ namespace Ryujinx.Headless.SDL2
private readonly long _ticksPerFrame; private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource; private readonly CancellationTokenSource _gpuCancellationTokenSource;
private readonly ManualResetEvent _exitEvent; private readonly ManualResetEvent _exitEvent;
private readonly ManualResetEvent _gpuDoneEvent;
private long _ticks; private long _ticks;
private bool _isActive; private bool _isActive;
@ -91,6 +92,7 @@ namespace Ryujinx.Headless.SDL2
_ticksPerFrame = Stopwatch.Frequency / TargetFps; _ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false); _exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false);
_aspectRatio = aspectRatio; _aspectRatio = aspectRatio;
_enableMouse = enableMouse; _enableMouse = enableMouse;
HostUiTheme = new HeadlessHostUiTheme(); HostUiTheme = new HeadlessHostUiTheme();
@ -275,6 +277,14 @@ namespace Ryujinx.Headless.SDL2
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame); _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
} }
} }
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
}); });
FinalizeWindowRenderer(); FinalizeWindowRenderer();
@ -404,7 +414,10 @@ namespace Ryujinx.Headless.SDL2
MainLoop(); MainLoop();
renderLoopThread.Join(); // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
nvStutterWorkaround?.Join(); nvStutterWorkaround?.Join();
Exit(); Exit();

View File

@ -343,7 +343,14 @@ namespace Ryujinx.Ui.App.Common
ulong nacpSize = reader.ReadUInt64(); ulong nacpSize = reader.ReadUInt64();
// Reads and stores game icon as byte array // Reads and stores game icon as byte array
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); if (iconSize > 0)
{
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
applicationIcon = _nroIcon;
}
// Read the NACP data // Read the NACP data
Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
@ -666,7 +673,14 @@ namespace Ryujinx.Ui.App.Common
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
// Reads and stores game icon as byte array // Reads and stores game icon as byte array
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); if (iconSize > 0)
{
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
}
else
{
applicationIcon = _nroIcon;
}
} }
else else
{ {

View File

@ -1,5 +1,6 @@
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Targets;
using System; using System;
namespace Ryujinx.Ui.Common.Configuration namespace Ryujinx.Ui.Common.Configuration

View File

@ -65,6 +65,7 @@ namespace Ryujinx.Ui
private KeyboardHotkeyState _prevHotkeyState; private KeyboardHotkeyState _prevHotkeyState;
private readonly ManualResetEvent _exitEvent; private readonly ManualResetEvent _exitEvent;
private readonly ManualResetEvent _gpuDoneEvent;
private readonly CancellationTokenSource _gpuCancellationTokenSource; private readonly CancellationTokenSource _gpuCancellationTokenSource;
@ -110,6 +111,7 @@ namespace Ryujinx.Ui
| EventMask.KeyReleaseMask)); | EventMask.KeyReleaseMask));
_exitEvent = new ManualResetEvent(false); _exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false);
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
@ -499,6 +501,14 @@ namespace Ryujinx.Ui
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame); _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
} }
} }
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
threaded.FlushThreadedCommands();
}
_gpuDoneEvent.Set();
}); });
} }
@ -542,7 +552,10 @@ namespace Ryujinx.Ui
MainLoop(); MainLoop();
renderLoopThread.Join(); // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
nvStutterWorkaround?.Join(); nvStutterWorkaround?.Join();
Exit(); Exit();