Przeglądaj źródła

Headless in Avalonia v2 (#448)

Launch the Ryujinx.exe, first argument --no-gui or nogui, and the rest of the arguments should be your normal headless script. You can include the new option --use-main-config which will provide any arguments that you don't, filled in from your main config made by the UI.

Input config is not inherited at this time.
Evan Husted 1 rok temu
rodzic
commit
12b264af44

+ 0 - 23
.github/workflows/build.yml

@@ -64,14 +64,9 @@ jobs:
         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 
         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
-        if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
-
       - name: Set executable bit
         run: |
           chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
-          chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/Ryujinx.sh
         if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
 
       - name: Build AppImage
@@ -119,13 +114,6 @@ jobs:
           name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}-AppImage
           path: publish_appimage
 
-      - name: Upload Ryujinx.Headless.SDL2 artifact
-        uses: actions/upload-artifact@v4
-        with:
-          name: nogui-ryujinx-${{ 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-13'
-
   build_macos:
     name: macOS Universal (${{ matrix.configuration }})
     runs-on: ubuntu-latest
@@ -171,20 +159,9 @@ jobs:
         run: |
           ./distribution/macos/create_macos_build_ava.sh . publish_tmp publish ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
 
-      - name: Publish macOS Ryujinx.Headless.SDL2
-        run: |
-          ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
-
       - name: Upload Ryujinx artifact
         uses: actions/upload-artifact@v4
         with:
           name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
           path: "publish/*.tar.gz"
         if: github.event_name == 'pull_request'
-
-      - name: Upload Ryujinx.Headless.SDL2 artifact
-        uses: actions/upload-artifact@v4
-        with:
-          name: nogui-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
-          path: "publish_headless/*.tar.gz"
-        if: github.event_name == 'pull_request'

+ 2 - 18
.github/workflows/canary.yml

@@ -116,7 +116,6 @@ jobs:
       - name: Publish
         run: |
           dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
-          dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained
 
       - name: Packing Windows builds
         if: matrix.platform.os == 'windows-latest'
@@ -125,11 +124,6 @@ jobs:
           rm publish/libarmeilleure-jitsupport.dylib
           7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
           popd
-
-          pushd publish_sdl2_headless
-          rm publish/libarmeilleure-jitsupport.dylib
-          7z a ../release_output/nogui-ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
-          popd
         shell: bash
 
       - name: Packing Linux builds
@@ -140,12 +134,6 @@ jobs:
           chmod +x publish/Ryujinx.sh publish/Ryujinx
           tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
           popd
-
-          pushd publish_sdl2_headless
-          rm publish/libarmeilleure-jitsupport.dylib
-          chmod +x publish/Ryujinx.sh publish/Ryujinx.Headless.SDL2
-          tar -czvf ../release_output/nogui-ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
-          popd
         shell: bash
       
       #- name: Build AppImage (Linux)
@@ -191,7 +179,7 @@ jobs:
         with:
           name: ${{ steps.version_info.outputs.build_version }}
           artifacts: "release_output/*.tar.gz,release_output/*.zip"
-          #artifacts: "release_output/*.tar.gz,release_output/*.zip/*AppImage*"
+          #artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*"
           tag: ${{ steps.version_info.outputs.build_version }}
           body: |
             # Canary builds:
@@ -262,15 +250,11 @@ jobs:
         run: |
           ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
 
-      - name: Publish macOS Ryujinx.Headless.SDL2
-        run: |
-          ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
-
       - name: Pushing new release
         uses: ncipollo/release-action@v1
         with:
           name: "Canary ${{ steps.version_info.outputs.build_version }}"
-          artifacts: "publish_ava/*.tar.gz, publish_headless/*.tar.gz"
+          artifacts: "publish_ava/*.tar.gz"
           tag: ${{ steps.version_info.outputs.build_version }}
           body: ""
           omitBodyDuringUpdate: true

+ 0 - 5
.github/workflows/nightly_pr_comment.yml

@@ -38,21 +38,16 @@ jobs:
               return core.error(`No artifacts found`);
             }
             let body = `Download the artifacts for this pull request:\n`;
-            let hidden_headless_artifacts = `\n\n <details><summary>GUI-less</summary>\n`;
             let hidden_debug_artifacts = `\n\n <details><summary>Only for Developers</summary>\n`;
             for (const art of artifacts) {
               const url = `https://github.com/Ryubing/Ryujinx/actions/runs/${run_id}/artifacts/${art.id}`;
               if(art.name.includes('Debug')) {
                 hidden_debug_artifacts += `\n* [${art.name}](${url})`;
-              } else if(art.name.includes('nogui-ryujinx')) {
-                hidden_headless_artifacts += `\n* [${art.name}](${url})`;
               } else {
                 body += `\n* [${art.name}](${url})`;
               }
             }
-            hidden_headless_artifacts += `\n</details>`;
             hidden_debug_artifacts += `\n</details>`;
-            body += hidden_headless_artifacts;
             body += hidden_debug_artifacts;
 
             const {data: comments} = await github.rest.issues.listComments({repo, owner, issue_number});

+ 1 - 16
.github/workflows/release.yml

@@ -112,7 +112,6 @@ jobs:
       - name: Publish
         run: |
           dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained
-          dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained
 
       - name: Packing Windows builds
         if: matrix.platform.os == 'windows-latest'
@@ -121,11 +120,6 @@ jobs:
           rm libarmeilleure-jitsupport.dylib
           7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
           popd
-
-          pushd publish_sdl2_headless
-          rm libarmeilleure-jitsupport.dylib
-          7z a ../release_output/nogui-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
-          popd
         shell: bash
       
       - name: Build AppImage (Linux)
@@ -172,11 +166,6 @@ jobs:
           chmod +x Ryujinx.sh Ryujinx
           tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
           popd
-
-          pushd publish_sdl2_headless
-          chmod +x Ryujinx.sh Ryujinx.Headless.SDL2
-          tar -czvf ../release_output/nogui-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
-          popd
         shell: bash
 
       - name: Pushing new release
@@ -251,15 +240,11 @@ jobs:
         run: |
           ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
 
-      - name: Publish macOS Ryujinx.Headless.SDL2
-        run: |
-          ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
-
       - name: Pushing new release
         uses: ncipollo/release-action@v1
         with:
           name: ${{ steps.version_info.outputs.build_version }}
-          artifacts: "publish/*.tar.gz, publish_headless/*.tar.gz"
+          artifacts: "publish/*.tar.gz"
           tag: ${{ steps.version_info.outputs.build_version }}
           body: ""
           omitBodyDuringUpdate: true

+ 0 - 6
Ryujinx.sln

@@ -57,8 +57,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.SDL2.Common", "src\
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SDL2", "src\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj", "{D99A395A-8569-4DB0-B336-900647890052}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Headless.SDL2", "src\Ryujinx.Headless.SDL2\Ryujinx.Headless.SDL2.csproj", "{390DC343-5CB4-4C79-A5DD-E3ED235E4C49}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec.FFmpeg", "src\Ryujinx.Graphics.Nvdec.FFmpeg\Ryujinx.Graphics.Nvdec.FFmpeg.csproj", "{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
@@ -213,10 +211,6 @@ Global
 		{D99A395A-8569-4DB0-B336-900647890052}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.Build.0 = Release|Any CPU
-		{390DC343-5CB4-4C79-A5DD-E3ED235E4C49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{390DC343-5CB4-4C79-A5DD-E3ED235E4C49}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{390DC343-5CB4-4C79-A5DD-E3ED235E4C49}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{390DC343-5CB4-4C79-A5DD-E3ED235E4C49}.Release|Any CPU.Build.0 = Release|Any CPU
 		{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU

+ 7 - 11
src/Ryujinx.Common/Logging/Logger.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
+using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Threading;
 
@@ -157,21 +158,16 @@ namespace Ryujinx.Common.Logging
             _time.Restart();
         }
 
-        private static ILogTarget GetTarget(string targetName)
+        private static ILogTarget GetTarget(string targetName) 
+            => _logTargets.FirstOrDefault(target => target.Name.Equals(targetName));
+
+        public static void AddTarget(ILogTarget target)
         {
-            foreach (var target in _logTargets)
+            if (_logTargets.Any(t => t.Name == target.Name))
             {
-                if (target.Name.Equals(targetName))
-                {
-                    return target;
-                }
+                return;
             }
 
-            return null;
-        }
-
-        public static void AddTarget(ILogTarget target)
-        {
             _logTargets.Add(target);
 
             Updated += target.Log;

+ 1 - 5
src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs

@@ -27,11 +27,7 @@ namespace Ryujinx.Common.Logging.Targets
 
         private readonly int _overflowTimeout;
 
-        string ILogTarget.Name { get => _target.Name; }
-
-        public AsyncLogTargetWrapper(ILogTarget target)
-            : this(target, -1)
-        { }
+        string ILogTarget.Name => _target.Name;
 
         public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block)
         {

+ 14 - 2
src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs

@@ -1,3 +1,4 @@
+using Gommon;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Utilities;
@@ -6,12 +7,13 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 
 namespace Ryujinx.HLE.HOS.Services.Account.Acc
 {
-    class AccountSaveDataManager
+    public class AccountSaveDataManager
     {
-        private readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json");
+        private static readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json");
 
         private static readonly ProfilesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
@@ -49,6 +51,16 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
             }
         }
 
+        public static Optional<UserProfile> GetLastUsedUser()
+        {
+            ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, _serializerContext.ProfilesJson);
+
+            return profilesJson.Profiles
+                .FindFirst(profile => profile.AccountState == AccountState.Open)
+                .Convert(profileJson => new UserProfile(new UserId(profileJson.UserId), profileJson.Name,
+                    profileJson.Image, profileJson.LastModifiedTimestamp));
+        }
+
         public void Save(ConcurrentDictionary<string, UserProfile> profiles)
         {
             ProfilesJson profilesJson = new()

+ 0 - 73
src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj

@@ -1,73 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
-    <OutputType>Exe</OutputType>
-    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <Version>1.0.0-dirty</Version>
-    <DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
-    <SigningCertificate Condition=" '$(SigningCertificate)' == '' ">-</SigningCertificate>
-    <TieredPGO>true</TieredPGO>
-    <DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <PackageReference Include="OpenTK.Core" />
-    <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
-  </ItemGroup>
-
-  <Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
-    <Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
-  </Target>
-
-  <ItemGroup>
-    <ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
-    <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
-    <ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
-    <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
-    <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
-    <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
-    <ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
-    <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
-    <ProjectReference Include="..\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj" />
-    <ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <PackageReference Include="CommandLineParser" />
-    <PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Content Include="..\..\distribution\legal\THIRDPARTY.md">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-      <TargetPath>THIRDPARTY.md</TargetPath>
-    </Content>
-    <Content Include="..\..\LICENSE.txt">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-      <TargetPath>LICENSE.txt</TargetPath>
-    </Content>
-  </ItemGroup>
-
-  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64' OR ('$(RuntimeIdentifier)' == '' AND $([MSBuild]::IsOSPlatform('Linux')))">
-    <Content Include="..\..\distribution\linux\Ryujinx.sh">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-
-  <ItemGroup>
-    <EmbeddedResource Include="Ryujinx.bmp" />
-  </ItemGroup>
-
-  <!-- Due to .net core 3.1 embedded resource loading -->
-  <PropertyGroup>
-    <EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
-    <ApplicationIcon>..\Ryujinx\Ryujinx.ico</ApplicationIcon>
-  </PropertyGroup>
-
-  <PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
-    <PublishSingleFile>true</PublishSingleFile>
-    <PublishTrimmed>true</PublishTrimmed>
-    <TrimMode>partial</TrimMode>
-  </PropertyGroup>
-</Project>

BIN
src/Ryujinx.Headless.SDL2/Ryujinx.bmp


+ 1 - 0
src/Ryujinx.UI.Common/Configuration/System/Language.cs

@@ -1,4 +1,5 @@
 using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.SystemState;
 using System.Text.Json.Serialization;
 
 namespace Ryujinx.UI.Common.Configuration.System

+ 1 - 1
src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs

@@ -132,7 +132,7 @@ namespace Ryujinx.UI.Common.Helper
 
                 if (uninstall)
                 {
-                    // If the types don't already exist, there's nothing to do and we can call this operation successful.
+                    // If the types don't already exist, there's nothing to do, and we can call this operation successful.
                     if (!AreMimeTypesRegisteredWindows())
                     {
                         return true;

+ 1 - 1
src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs → src/Ryujinx/Headless/HeadlessDynamicTextInputHandler.cs

@@ -2,7 +2,7 @@ using Ryujinx.HLE.UI;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace Ryujinx.Headless.SDL2
+namespace Ryujinx.Headless
 {
     /// <summary>
     /// Headless text processing class, right now there is no way to forward the input to it.

+ 1 - 1
src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs → src/Ryujinx/Headless/HeadlessHostUiTheme.cs

@@ -1,6 +1,6 @@
 using Ryujinx.HLE.UI;
 
-namespace Ryujinx.Headless.SDL2
+namespace Ryujinx.Headless
 {
     internal class HeadlessHostUiTheme : IHostUITheme
     {

+ 367 - 0
src/Ryujinx/Headless/HeadlessRyujinx.Init.cs

@@ -0,0 +1,367 @@
+using DiscordRPC;
+using LibHac.Tools.FsSystem;
+using Ryujinx.Audio.Backends.SDL2;
+using Ryujinx.Ava;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Controller.Motion;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.Graphics.Metal;
+using Ryujinx.Graphics.OpenGL;
+using Ryujinx.Graphics.Vulkan;
+using Ryujinx.HLE;
+using Ryujinx.Input;
+using Ryujinx.UI.Common;
+using Ryujinx.UI.Common.Configuration;
+using Silk.NET.Vulkan;
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
+using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
+using Key = Ryujinx.Common.Configuration.Hid.Key;
+
+namespace Ryujinx.Headless
+{
+    public partial class HeadlessRyujinx
+    {
+        public static void Initialize()
+        {
+            // Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
+            DiscordIntegrationModule.StartedAt = Timestamps.Now;
+
+            // Delete backup files after updating.
+            Task.Run(Updater.CleanupUpdate);
+
+            // Hook unhandled exception and process exit events.
+            AppDomain.CurrentDomain.UnhandledException += (sender, e)
+                => Program.ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating);
+            AppDomain.CurrentDomain.ProcessExit += (_, _) => Program.Exit();
+
+            // Initialize the configuration.
+            ConfigurationState.Initialize();
+
+            // Initialize Discord integration.
+            DiscordIntegrationModule.Initialize();
+
+            // Logging system information.
+            Program.PrintSystemInfo();
+        }
+        
+        private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
+        {
+            if (inputId == null)
+            {
+                if (index == PlayerIndex.Player1)
+                {
+                    Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard.");
+
+                    // Default to keyboard
+                    inputId = "0";
+                }
+                else
+                {
+                    Logger.Info?.Print(LogClass.Application, $"{index} not configured");
+
+                    return null;
+                }
+            }
+
+            IGamepad gamepad = _inputManager.KeyboardDriver.GetGamepad(inputId);
+
+            bool isKeyboard = true;
+
+            if (gamepad == null)
+            {
+                gamepad = _inputManager.GamepadDriver.GetGamepad(inputId);
+                isKeyboard = false;
+
+                if (gamepad == null)
+                {
+                    Logger.Error?.Print(LogClass.Application, $"{index} gamepad not found (\"{inputId}\")");
+
+                    return null;
+                }
+            }
+
+            string gamepadName = gamepad.Name;
+
+            gamepad.Dispose();
+
+            InputConfig config;
+
+            if (inputProfileName == null || inputProfileName.Equals("default"))
+            {
+                if (isKeyboard)
+                {
+                    config = new StandardKeyboardInputConfig
+                    {
+                        Version = InputConfig.CurrentVersion,
+                        Backend = InputBackendType.WindowKeyboard,
+                        Id = null,
+                        ControllerType = ControllerType.JoyconPair,
+                        LeftJoycon = new LeftJoyconCommonConfig<Key>
+                        {
+                            DpadUp = Key.Up,
+                            DpadDown = Key.Down,
+                            DpadLeft = Key.Left,
+                            DpadRight = Key.Right,
+                            ButtonMinus = Key.Minus,
+                            ButtonL = Key.E,
+                            ButtonZl = Key.Q,
+                            ButtonSl = Key.Unbound,
+                            ButtonSr = Key.Unbound,
+                        },
+
+                        LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
+                        {
+                            StickUp = Key.W,
+                            StickDown = Key.S,
+                            StickLeft = Key.A,
+                            StickRight = Key.D,
+                            StickButton = Key.F,
+                        },
+
+                        RightJoycon = new RightJoyconCommonConfig<Key>
+                        {
+                            ButtonA = Key.Z,
+                            ButtonB = Key.X,
+                            ButtonX = Key.C,
+                            ButtonY = Key.V,
+                            ButtonPlus = Key.Plus,
+                            ButtonR = Key.U,
+                            ButtonZr = Key.O,
+                            ButtonSl = Key.Unbound,
+                            ButtonSr = Key.Unbound,
+                        },
+
+                        RightJoyconStick = new JoyconConfigKeyboardStick<Key>
+                        {
+                            StickUp = Key.I,
+                            StickDown = Key.K,
+                            StickLeft = Key.J,
+                            StickRight = Key.L,
+                            StickButton = Key.H,
+                        },
+                    };
+                }
+                else
+                {
+                    bool isNintendoStyle = gamepadName.Contains("Nintendo");
+
+                    config = new StandardControllerInputConfig
+                    {
+                        Version = InputConfig.CurrentVersion,
+                        Backend = InputBackendType.GamepadSDL2,
+                        Id = null,
+                        ControllerType = ControllerType.JoyconPair,
+                        DeadzoneLeft = 0.1f,
+                        DeadzoneRight = 0.1f,
+                        RangeLeft = 1.0f,
+                        RangeRight = 1.0f,
+                        TriggerThreshold = 0.5f,
+                        LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
+                        {
+                            DpadUp = ConfigGamepadInputId.DpadUp,
+                            DpadDown = ConfigGamepadInputId.DpadDown,
+                            DpadLeft = ConfigGamepadInputId.DpadLeft,
+                            DpadRight = ConfigGamepadInputId.DpadRight,
+                            ButtonMinus = ConfigGamepadInputId.Minus,
+                            ButtonL = ConfigGamepadInputId.LeftShoulder,
+                            ButtonZl = ConfigGamepadInputId.LeftTrigger,
+                            ButtonSl = ConfigGamepadInputId.Unbound,
+                            ButtonSr = ConfigGamepadInputId.Unbound,
+                        },
+
+                        LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
+                        {
+                            Joystick = ConfigStickInputId.Left,
+                            StickButton = ConfigGamepadInputId.LeftStick,
+                            InvertStickX = false,
+                            InvertStickY = false,
+                            Rotate90CW = false,
+                        },
+
+                        RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
+                        {
+                            ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
+                            ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
+                            ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
+                            ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
+                            ButtonPlus = ConfigGamepadInputId.Plus,
+                            ButtonR = ConfigGamepadInputId.RightShoulder,
+                            ButtonZr = ConfigGamepadInputId.RightTrigger,
+                            ButtonSl = ConfigGamepadInputId.Unbound,
+                            ButtonSr = ConfigGamepadInputId.Unbound,
+                        },
+
+                        RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
+                        {
+                            Joystick = ConfigStickInputId.Right,
+                            StickButton = ConfigGamepadInputId.RightStick,
+                            InvertStickX = false,
+                            InvertStickY = false,
+                            Rotate90CW = false,
+                        },
+
+                        Motion = new StandardMotionConfigController
+                        {
+                            MotionBackend = MotionInputBackendType.GamepadDriver,
+                            EnableMotion = true,
+                            Sensitivity = 100,
+                            GyroDeadzone = 1,
+                        },
+                        Rumble = new RumbleConfigController
+                        {
+                            StrongRumble = 1f,
+                            WeakRumble = 1f,
+                            EnableRumble = false,
+                        },
+                    };
+                }
+            }
+            else
+            {
+                string profileBasePath;
+
+                if (isKeyboard)
+                {
+                    profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "keyboard");
+                }
+                else
+                {
+                    profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "controller");
+                }
+
+                string path = Path.Combine(profileBasePath, inputProfileName + ".json");
+
+                if (!File.Exists(path))
+                {
+                    Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" not found for \"{inputId}\"");
+
+                    return null;
+                }
+
+                try
+                {
+                    config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig);
+                }
+                catch (JsonException)
+                {
+                    Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" parsing failed for \"{inputId}\"");
+
+                    return null;
+                }
+            }
+
+            config.Id = inputId;
+            config.PlayerIndex = index;
+
+            string inputTypeName = isKeyboard ? "Keyboard" : "Gamepad";
+
+            Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} configured with {inputTypeName} \"{config.Id}\"");
+
+            // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0.
+            if (config is StandardControllerInputConfig controllerConfig)
+            {
+                if (controllerConfig.RangeLeft <= 0.0f && controllerConfig.RangeRight <= 0.0f)
+                {
+                    controllerConfig.RangeLeft = 1.0f;
+                    controllerConfig.RangeRight = 1.0f;
+
+                    Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration");
+                }
+            }
+
+            return config;
+        }
+        
+        private static IRenderer CreateRenderer(Options options, WindowBase window)
+        {
+            if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow)
+            {
+                string preferredGpuId = string.Empty;
+                Vk api = Vk.GetApi();
+
+                if (!string.IsNullOrEmpty(options.PreferredGPUVendor))
+                {
+                    string preferredGpuVendor = options.PreferredGPUVendor.ToLowerInvariant();
+                    var devices = VulkanRenderer.GetPhysicalDevices(api);
+
+                    foreach (var device in devices)
+                    {
+                        if (device.Vendor.ToLowerInvariant() == preferredGpuVendor)
+                        {
+                            preferredGpuId = device.Id;
+                            break;
+                        }
+                    }
+                }
+
+                return new VulkanRenderer(
+                    api,
+                    (instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
+                    vulkanWindow.GetRequiredInstanceExtensions,
+                    preferredGpuId);
+            }
+
+            if (options.GraphicsBackend == GraphicsBackend.Metal && window is MetalWindow metalWindow && OperatingSystem.IsMacOS())
+            {
+                return new MetalRenderer(metalWindow.GetLayer);
+            }
+
+            return new OpenGLRenderer();
+        }
+
+        private static Switch InitializeEmulationContext(WindowBase window, IRenderer renderer, Options options)
+        {
+            BackendThreading threadingMode = options.BackendThreading;
+
+            bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
+
+            if (threadedGAL)
+            {
+                renderer = new ThreadedRenderer(renderer);
+            }
+
+            HLEConfiguration configuration = new(_virtualFileSystem,
+                _libHacHorizonManager,
+                _contentManager,
+                _accountManager,
+                _userChannelPersistence,
+                renderer,
+                new SDL2HardwareDeviceDriver(),
+                options.DramSize,
+                window,
+                options.SystemLanguage,
+                options.SystemRegion,
+                options.VSyncMode,
+                !options.DisableDockedMode,
+                !options.DisablePTC,
+                options.EnableInternetAccess,
+                !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
+                options.FsGlobalAccessLogMode,
+                options.SystemTimeOffset,
+                options.SystemTimeZone,
+                options.MemoryManagerMode,
+                options.IgnoreMissingServices,
+                options.AspectRatio,
+                options.AudioVolume,
+                options.UseHypervisor ?? true,
+                options.MultiplayerLanInterfaceId,
+                Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
+                false,
+                string.Empty,
+                string.Empty,
+                options.CustomVSyncInterval);
+
+            return new Switch(configuration);
+        }
+    }
+}

+ 64 - 321
src/Ryujinx.Headless.SDL2/Program.cs → src/Ryujinx/Headless/HeadlessRyujinx.cs

@@ -1,13 +1,9 @@
 using CommandLine;
 using Gommon;
-using LibHac.Tools.FsSystem;
-using Ryujinx.Audio.Backends.SDL2;
+using Ryujinx.Ava;
 using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
-using Ryujinx.Common.Configuration.Hid.Controller;
-using Ryujinx.Common.Configuration.Hid.Controller.Motion;
-using Ryujinx.Common.Configuration.Hid.Keyboard;
 using Ryujinx.Common.GraphicsDriver;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Logging.Targets;
@@ -15,16 +11,12 @@ using Ryujinx.Common.SystemInterop;
 using Ryujinx.Common.Utilities;
 using Ryujinx.Cpu;
 using Ryujinx.Graphics.GAL;
-using Ryujinx.Graphics.GAL.Multithreading;
 using Ryujinx.Graphics.Gpu;
 using Ryujinx.Graphics.Gpu.Shader;
 using Ryujinx.Graphics.Metal;
 using Ryujinx.Graphics.OpenGL;
 using Ryujinx.Graphics.Vulkan;
 using Ryujinx.Graphics.Vulkan.MoltenVK;
-using Ryujinx.Headless.SDL2.Metal;
-using Ryujinx.Headless.SDL2.OpenGL;
-using Ryujinx.Headless.SDL2.Vulkan;
 using Ryujinx.HLE;
 using Ryujinx.HLE.FileSystem;
 using Ryujinx.HLE.HOS;
@@ -33,22 +25,16 @@ using Ryujinx.Input;
 using Ryujinx.Input.HLE;
 using Ryujinx.Input.SDL2;
 using Ryujinx.SDL2.Common;
-using Silk.NET.Vulkan;
+using Ryujinx.UI.Common.Configuration;
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Text.Json;
 using System.Threading;
-using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
-using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
-using Key = Ryujinx.Common.Configuration.Hid.Key;
 
-namespace Ryujinx.Headless.SDL2
+namespace Ryujinx.Headless
 {
-    class Program
+    public partial class HeadlessRyujinx
     {
-        public static string Version { get; private set; }
-
         private static VirtualFileSystem _virtualFileSystem;
         private static ContentManager _contentManager;
         private static AccountManager _accountManager;
@@ -58,20 +44,18 @@ namespace Ryujinx.Headless.SDL2
         private static Switch _emulationContext;
         private static WindowBase _window;
         private static WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
-        private static List<InputConfig> _inputConfiguration;
+        private static List<InputConfig> _inputConfiguration = [];
         private static bool _enableKeyboard;
         private static bool _enableMouse;
 
         private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
-        static void Main(string[] args)
+        public static void Entrypoint(string[] args)
         {
-            Version = ReleaseInformation.Version;
-
             // Make process DPI aware for proper window sizing on high-res screens.
             ForceDpiAware.Windows();
 
-            Console.Title = $"Ryujinx Console {Version} (Headless SDL2)";
+            Console.Title = $"Ryujinx Console {Program.Version} (Headless)";
 
             if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
             {
@@ -99,7 +83,7 @@ namespace Ryujinx.Headless.SDL2
             }
 
             Parser.Default.ParseArguments<Options>(args)
-                .WithParsed(Load)
+                .WithParsed(options => Load(args, options))
                 .WithNotParsed(errors =>
                 {
                     Logger.Error?.PrintMsg(LogClass.Application, "Error parsing command-line arguments:");
@@ -107,239 +91,81 @@ namespace Ryujinx.Headless.SDL2
                     errors.ForEach(err => Logger.Error?.PrintMsg(LogClass.Application, $" - {err.Tag}"));
                 });
         }
-
-        private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
+        
+        public static void ReloadConfig(string customConfigPath = null)
         {
-            if (inputId == null)
-            {
-                if (index == PlayerIndex.Player1)
-                {
-                    Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard.");
-
-                    // Default to keyboard
-                    inputId = "0";
-                }
-                else
-                {
-                    Logger.Info?.Print(LogClass.Application, $"{index} not configured");
+            string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
+            string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
 
-                    return null;
-                }
+            string configurationPath = null;
+            
+            // Now load the configuration as the other subsystems are now registered
+            if (customConfigPath != null && File.Exists(customConfigPath))
+            {
+                configurationPath = customConfigPath;
+            } 
+            else if (File.Exists(localConfigurationPath))
+            {
+                configurationPath = localConfigurationPath;
             }
-
-            IGamepad gamepad = _inputManager.KeyboardDriver.GetGamepad(inputId);
-
-            bool isKeyboard = true;
-
-            if (gamepad == null)
+            else if (File.Exists(appDataConfigurationPath))
             {
-                gamepad = _inputManager.GamepadDriver.GetGamepad(inputId);
-                isKeyboard = false;
-
-                if (gamepad == null)
-                {
-                    Logger.Error?.Print(LogClass.Application, $"{index} gamepad not found (\"{inputId}\")");
-
-                    return null;
-                }
+                configurationPath = appDataConfigurationPath;
             }
 
-            string gamepadName = gamepad.Name;
-
-            gamepad.Dispose();
-
-            InputConfig config;
-
-            if (inputProfileName == null || inputProfileName.Equals("default"))
+            if (configurationPath == null)
             {
-                if (isKeyboard)
-                {
-                    config = new StandardKeyboardInputConfig
-                    {
-                        Version = InputConfig.CurrentVersion,
-                        Backend = InputBackendType.WindowKeyboard,
-                        Id = null,
-                        ControllerType = ControllerType.JoyconPair,
-                        LeftJoycon = new LeftJoyconCommonConfig<Key>
-                        {
-                            DpadUp = Key.Up,
-                            DpadDown = Key.Down,
-                            DpadLeft = Key.Left,
-                            DpadRight = Key.Right,
-                            ButtonMinus = Key.Minus,
-                            ButtonL = Key.E,
-                            ButtonZl = Key.Q,
-                            ButtonSl = Key.Unbound,
-                            ButtonSr = Key.Unbound,
-                        },
-
-                        LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
-                        {
-                            StickUp = Key.W,
-                            StickDown = Key.S,
-                            StickLeft = Key.A,
-                            StickRight = Key.D,
-                            StickButton = Key.F,
-                        },
-
-                        RightJoycon = new RightJoyconCommonConfig<Key>
-                        {
-                            ButtonA = Key.Z,
-                            ButtonB = Key.X,
-                            ButtonX = Key.C,
-                            ButtonY = Key.V,
-                            ButtonPlus = Key.Plus,
-                            ButtonR = Key.U,
-                            ButtonZr = Key.O,
-                            ButtonSl = Key.Unbound,
-                            ButtonSr = Key.Unbound,
-                        },
-
-                        RightJoyconStick = new JoyconConfigKeyboardStick<Key>
-                        {
-                            StickUp = Key.I,
-                            StickDown = Key.K,
-                            StickLeft = Key.J,
-                            StickRight = Key.L,
-                            StickButton = Key.H,
-                        },
-                    };
-                }
-                else
-                {
-                    bool isNintendoStyle = gamepadName.Contains("Nintendo");
+                // No configuration, we load the default values and save it to disk
+                configurationPath = appDataConfigurationPath;
+                Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {configurationPath}");
 
-                    config = new StandardControllerInputConfig
-                    {
-                        Version = InputConfig.CurrentVersion,
-                        Backend = InputBackendType.GamepadSDL2,
-                        Id = null,
-                        ControllerType = ControllerType.JoyconPair,
-                        DeadzoneLeft = 0.1f,
-                        DeadzoneRight = 0.1f,
-                        RangeLeft = 1.0f,
-                        RangeRight = 1.0f,
-                        TriggerThreshold = 0.5f,
-                        LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
-                        {
-                            DpadUp = ConfigGamepadInputId.DpadUp,
-                            DpadDown = ConfigGamepadInputId.DpadDown,
-                            DpadLeft = ConfigGamepadInputId.DpadLeft,
-                            DpadRight = ConfigGamepadInputId.DpadRight,
-                            ButtonMinus = ConfigGamepadInputId.Minus,
-                            ButtonL = ConfigGamepadInputId.LeftShoulder,
-                            ButtonZl = ConfigGamepadInputId.LeftTrigger,
-                            ButtonSl = ConfigGamepadInputId.Unbound,
-                            ButtonSr = ConfigGamepadInputId.Unbound,
-                        },
-
-                        LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
-                        {
-                            Joystick = ConfigStickInputId.Left,
-                            StickButton = ConfigGamepadInputId.LeftStick,
-                            InvertStickX = false,
-                            InvertStickY = false,
-                            Rotate90CW = false,
-                        },
-
-                        RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
-                        {
-                            ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
-                            ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
-                            ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
-                            ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
-                            ButtonPlus = ConfigGamepadInputId.Plus,
-                            ButtonR = ConfigGamepadInputId.RightShoulder,
-                            ButtonZr = ConfigGamepadInputId.RightTrigger,
-                            ButtonSl = ConfigGamepadInputId.Unbound,
-                            ButtonSr = ConfigGamepadInputId.Unbound,
-                        },
-
-                        RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
-                        {
-                            Joystick = ConfigStickInputId.Right,
-                            StickButton = ConfigGamepadInputId.RightStick,
-                            InvertStickX = false,
-                            InvertStickY = false,
-                            Rotate90CW = false,
-                        },
-
-                        Motion = new StandardMotionConfigController
-                        {
-                            MotionBackend = MotionInputBackendType.GamepadDriver,
-                            EnableMotion = true,
-                            Sensitivity = 100,
-                            GyroDeadzone = 1,
-                        },
-                        Rumble = new RumbleConfigController
-                        {
-                            StrongRumble = 1f,
-                            WeakRumble = 1f,
-                            EnableRumble = false,
-                        },
-                    };
-                }
+                ConfigurationState.Instance.LoadDefault();
+                ConfigurationState.Instance.ToFileFormat().SaveConfig(configurationPath);
             }
             else
             {
-                string profileBasePath;
+                Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {configurationPath}");
 
-                if (isKeyboard)
+                if (ConfigurationFileFormat.TryLoad(configurationPath, out ConfigurationFileFormat configurationFileFormat))
                 {
-                    profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "keyboard");
+                    ConfigurationState.Instance.Load(configurationFileFormat, configurationPath);
                 }
                 else
                 {
-                    profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "controller");
-                }
-
-                string path = Path.Combine(profileBasePath, inputProfileName + ".json");
-
-                if (!File.Exists(path))
-                {
-                    Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" not found for \"{inputId}\"");
-
-                    return null;
-                }
-
-                try
-                {
-                    config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig);
-                }
-                catch (JsonException)
-                {
-                    Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" parsing failed for \"{inputId}\"");
+                    Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {configurationPath}");
 
-                    return null;
+                    ConfigurationState.Instance.LoadDefault();
                 }
             }
+        }
 
-            config.Id = inputId;
-            config.PlayerIndex = index;
+        static void Load(string[] originalArgs, Options option)
+        {
+            Initialize();
 
-            string inputTypeName = isKeyboard ? "Keyboard" : "Gamepad";
+            bool useLastUsedProfile = false;
 
-            Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} configured with {inputTypeName} \"{config.Id}\"");
+            if (option.InheritConfig)
+            {
+                option.InheritMainConfig(originalArgs, ConfigurationState.Instance, out useLastUsedProfile);
+            }
 
-            // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0.
-            if (config is StandardControllerInputConfig controllerConfig)
+            AppDataManager.Initialize(option.BaseDataDir);
+            
+            if (useLastUsedProfile && AccountSaveDataManager.GetLastUsedUser().TryGet(out var profile))
+                option.UserProfile = profile.Name;
+            
+            // Check if keys exists.
+            if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
             {
-                if (controllerConfig.RangeLeft <= 0.0f && controllerConfig.RangeRight <= 0.0f)
+                if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
                 {
-                    controllerConfig.RangeLeft = 1.0f;
-                    controllerConfig.RangeRight = 1.0f;
-
-                    Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration");
+                    Logger.Error?.Print(LogClass.Application, "Keys not found");
                 }
             }
-
-            return config;
-        }
-
-        static void Load(Options option)
-        {
-            AppDataManager.Initialize(option.BaseDataDir);
-
+            
+            ReloadConfig();
+            
             _virtualFileSystem = VirtualFileSystem.CreateInstance();
             _libHacHorizonManager = new LibHacHorizonManager();
 
@@ -354,7 +180,7 @@ namespace Ryujinx.Headless.SDL2
 
             _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
 
-            GraphicsConfig.EnableShaderCache = true;
+            GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;
 
             if (OperatingSystem.IsMacOS())
             {
@@ -365,15 +191,13 @@ namespace Ryujinx.Headless.SDL2
                 }
             }
 
-            IGamepad gamepad;
-
             if (option.ListInputIds)
             {
                 Logger.Info?.Print(LogClass.Application, "Input Ids:");
 
                 foreach (string id in _inputManager.KeyboardDriver.GamepadsIds)
                 {
-                    gamepad = _inputManager.KeyboardDriver.GetGamepad(id);
+                    IGamepad gamepad = _inputManager.KeyboardDriver.GetGamepad(id);
 
                     Logger.Info?.Print(LogClass.Application, $"- {id} (\"{gamepad.Name}\")");
 
@@ -382,7 +206,7 @@ namespace Ryujinx.Headless.SDL2
 
                 foreach (string id in _inputManager.GamepadDriver.GamepadsIds)
                 {
-                    gamepad = _inputManager.GamepadDriver.GetGamepad(id);
+                    IGamepad gamepad = _inputManager.GamepadDriver.GetGamepad(id);
 
                     Logger.Info?.Print(LogClass.Application, $"- {id} (\"{gamepad.Name}\")");
 
@@ -399,7 +223,7 @@ namespace Ryujinx.Headless.SDL2
                 return;
             }
 
-            _inputConfiguration = new List<InputConfig>();
+            _inputConfiguration ??= [];
             _enableKeyboard = option.EnableKeyboard;
             _enableMouse = option.EnableMouse;
 
@@ -412,9 +236,9 @@ namespace Ryujinx.Headless.SDL2
                     _inputConfiguration.Add(inputConfig);
                 }
             }
-
+            
             LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1);
-            LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2);
+            LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2); 
             LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3);
             LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4);
             LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5);
@@ -422,6 +246,7 @@ namespace Ryujinx.Headless.SDL2
             LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7);
             LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8);
             LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld);
+            
 
             if (_inputConfiguration.Count == 0)
             {
@@ -433,7 +258,7 @@ namespace Ryujinx.Headless.SDL2
             Logger.SetEnable(LogLevel.Stub, !option.LoggingDisableStub);
             Logger.SetEnable(LogLevel.Info, !option.LoggingDisableInfo);
             Logger.SetEnable(LogLevel.Warning, !option.LoggingDisableWarning);
-            Logger.SetEnable(LogLevel.Error, option.LoggingEnableError);
+            Logger.SetEnable(LogLevel.Error, !option.LoggingDisableError);
             Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace);
             Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest);
             Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog);
@@ -522,88 +347,6 @@ namespace Ryujinx.Headless.SDL2
             };
         }
 
-        private static IRenderer CreateRenderer(Options options, WindowBase window)
-        {
-            if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow)
-            {
-                string preferredGpuId = string.Empty;
-                Vk api = Vk.GetApi();
-
-                if (!string.IsNullOrEmpty(options.PreferredGPUVendor))
-                {
-                    string preferredGpuVendor = options.PreferredGPUVendor.ToLowerInvariant();
-                    var devices = VulkanRenderer.GetPhysicalDevices(api);
-
-                    foreach (var device in devices)
-                    {
-                        if (device.Vendor.ToLowerInvariant() == preferredGpuVendor)
-                        {
-                            preferredGpuId = device.Id;
-                            break;
-                        }
-                    }
-                }
-
-                return new VulkanRenderer(
-                    api,
-                    (instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
-                    vulkanWindow.GetRequiredInstanceExtensions,
-                    preferredGpuId);
-            }
-
-            if (options.GraphicsBackend == GraphicsBackend.Metal && window is MetalWindow metalWindow && OperatingSystem.IsMacOS())
-            {
-                return new MetalRenderer(metalWindow.GetLayer);
-            }
-
-            return new OpenGLRenderer();
-        }
-
-        private static Switch InitializeEmulationContext(WindowBase window, IRenderer renderer, Options options)
-        {
-            BackendThreading threadingMode = options.BackendThreading;
-
-            bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
-
-            if (threadedGAL)
-            {
-                renderer = new ThreadedRenderer(renderer);
-            }
-
-            HLEConfiguration configuration = new(_virtualFileSystem,
-                _libHacHorizonManager,
-                _contentManager,
-                _accountManager,
-                _userChannelPersistence,
-                renderer,
-                new SDL2HardwareDeviceDriver(),
-                options.DramSize,
-                window,
-                options.SystemLanguage,
-                options.SystemRegion,
-                options.VSyncMode,
-                !options.DisableDockedMode,
-                !options.DisablePTC,
-                options.EnableInternetAccess,
-                !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
-                options.FsGlobalAccessLogMode,
-                options.SystemTimeOffset,
-                options.SystemTimeZone,
-                options.MemoryManagerMode,
-                options.IgnoreMissingServices,
-                options.AspectRatio,
-                options.AudioVolume,
-                options.UseHypervisor ?? true,
-                options.MultiplayerLanInterfaceId,
-                Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
-                false,
-                string.Empty,
-                string.Empty,
-                options.CustomVSyncInterval);
-
-            return new Switch(configuration);
-        }
-
         private static void ExecutionEntrypoint()
         {
             if (OperatingSystem.IsWindows())

+ 1 - 1
src/Ryujinx.Headless.SDL2/Metal/MetalWindow.cs → src/Ryujinx/Headless/Metal/MetalWindow.cs

@@ -5,7 +5,7 @@ using SharpMetal.QuartzCore;
 using System.Runtime.Versioning;
 using static SDL2.SDL;
 
-namespace Ryujinx.Headless.SDL2.Metal
+namespace Ryujinx.Headless
 {
     [SupportedOSPlatform("macos")]
     class MetalWindow : WindowBase

+ 1 - 1
src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs → src/Ryujinx/Headless/OpenGL/OpenGLWindow.cs

@@ -7,7 +7,7 @@ using Ryujinx.Input.HLE;
 using System;
 using static SDL2.SDL;
 
-namespace Ryujinx.Headless.SDL2.OpenGL
+namespace Ryujinx.Headless
 {
     class OpenGLWindow : WindowBase
     {

+ 157 - 2
src/Ryujinx.Headless.SDL2/Options.cs → src/Ryujinx/Headless/Options.cs

@@ -1,13 +1,168 @@
 using CommandLine;
+using Gommon;
 using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Hid;
 using Ryujinx.HLE;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
 using Ryujinx.HLE.HOS.SystemState;
+using Ryujinx.UI.Common.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
 
-namespace Ryujinx.Headless.SDL2
+namespace Ryujinx.Headless
 {
     public class Options
     {
+        public void InheritMainConfig(string[] originalArgs, ConfigurationState configurationState, out bool needsProfileSet)
+        {
+            needsProfileSet = NeedsOverride(nameof(UserProfile));
+
+            if (NeedsOverride(nameof(IsFullscreen)))
+                IsFullscreen = configurationState.UI.StartFullscreen;
+
+            if (NeedsOverride(nameof(EnableKeyboard)))
+                EnableKeyboard = configurationState.Hid.EnableKeyboard;
+            
+            if (NeedsOverride(nameof(EnableMouse)))
+                EnableMouse = configurationState.Hid.EnableMouse;
+
+            if (NeedsOverride(nameof(HideCursorMode)))
+                HideCursorMode = configurationState.HideCursor;
+
+            if (NeedsOverride(nameof(DisablePTC)))
+                DisablePTC = !configurationState.System.EnablePtc;
+
+            if (NeedsOverride(nameof(EnableInternetAccess)))
+                EnableInternetAccess = configurationState.System.EnableInternetAccess;
+
+            if (NeedsOverride(nameof(DisableFsIntegrityChecks)))
+                DisableFsIntegrityChecks = configurationState.System.EnableFsIntegrityChecks;
+            
+            if (NeedsOverride(nameof(FsGlobalAccessLogMode)))
+                FsGlobalAccessLogMode = configurationState.System.FsGlobalAccessLogMode;
+            
+            if (NeedsOverride(nameof(VSyncMode)))
+                VSyncMode = configurationState.Graphics.VSyncMode;
+            
+            if (NeedsOverride(nameof(CustomVSyncInterval)))
+                CustomVSyncInterval = configurationState.Graphics.CustomVSyncInterval;
+            
+            if (NeedsOverride(nameof(DisableShaderCache)))
+                DisableShaderCache = !configurationState.Graphics.EnableShaderCache;
+
+            if (NeedsOverride(nameof(EnableTextureRecompression)))
+                EnableTextureRecompression = configurationState.Graphics.EnableTextureRecompression;
+            
+            if (NeedsOverride(nameof(DisableDockedMode)))
+                DisableDockedMode = !configurationState.System.EnableDockedMode;
+
+            if (NeedsOverride(nameof(SystemLanguage)))
+                SystemLanguage = (SystemLanguage)(int)configurationState.System.Language.Value;
+            
+            if (NeedsOverride(nameof(SystemRegion)))
+                SystemRegion = (RegionCode)(int)configurationState.System.Region.Value;
+            
+            if (NeedsOverride(nameof(SystemTimeZone)))
+                SystemTimeZone = configurationState.System.TimeZone;
+            
+            if (NeedsOverride(nameof(SystemTimeOffset)))
+                SystemTimeOffset = configurationState.System.SystemTimeOffset;
+            
+            if (NeedsOverride(nameof(MemoryManagerMode)))
+                MemoryManagerMode = configurationState.System.MemoryManagerMode;
+            
+            if (NeedsOverride(nameof(AudioVolume)))
+                AudioVolume = configurationState.System.AudioVolume;
+
+            if (NeedsOverride(nameof(UseHypervisor)) && OperatingSystem.IsMacOS())
+                UseHypervisor = configurationState.System.UseHypervisor;
+
+            if (NeedsOverride(nameof(MultiplayerLanInterfaceId)))
+                MultiplayerLanInterfaceId = configurationState.Multiplayer.LanInterfaceId;
+            
+            if (NeedsOverride(nameof(DisableFileLog)))
+                DisableFileLog = !configurationState.Logger.EnableFileLog;
+            
+            if (NeedsOverride(nameof(LoggingEnableDebug)))
+                LoggingEnableDebug = configurationState.Logger.EnableDebug;
+            
+            if (NeedsOverride(nameof(LoggingDisableStub)))
+                LoggingDisableStub = !configurationState.Logger.EnableStub;
+            
+            if (NeedsOverride(nameof(LoggingDisableInfo)))
+                LoggingDisableInfo = !configurationState.Logger.EnableInfo;
+            
+            if (NeedsOverride(nameof(LoggingDisableWarning)))
+                LoggingDisableWarning = !configurationState.Logger.EnableWarn;
+            
+            if (NeedsOverride(nameof(LoggingDisableError)))
+                LoggingDisableError = !configurationState.Logger.EnableError;
+            
+            if (NeedsOverride(nameof(LoggingEnableTrace)))
+                LoggingEnableTrace = configurationState.Logger.EnableTrace;
+            
+            if (NeedsOverride(nameof(LoggingDisableGuest)))
+                LoggingDisableGuest = !configurationState.Logger.EnableGuest;
+
+            if (NeedsOverride(nameof(LoggingEnableFsAccessLog)))
+                LoggingEnableFsAccessLog = configurationState.Logger.EnableFsAccessLog;
+
+            if (NeedsOverride(nameof(LoggingGraphicsDebugLevel)))
+                LoggingGraphicsDebugLevel = configurationState.Logger.GraphicsDebugLevel;
+
+            if (NeedsOverride(nameof(ResScale)))
+                ResScale = configurationState.Graphics.ResScale;
+            
+            if (NeedsOverride(nameof(MaxAnisotropy)))
+                MaxAnisotropy = configurationState.Graphics.MaxAnisotropy;
+            
+            if (NeedsOverride(nameof(AspectRatio)))
+                AspectRatio = configurationState.Graphics.AspectRatio;
+            
+            if (NeedsOverride(nameof(BackendThreading)))
+                BackendThreading = configurationState.Graphics.BackendThreading;
+            
+            if (NeedsOverride(nameof(DisableMacroHLE)))
+                DisableMacroHLE = !configurationState.Graphics.EnableMacroHLE;
+            
+            if (NeedsOverride(nameof(GraphicsShadersDumpPath)))
+                GraphicsShadersDumpPath = configurationState.Graphics.ShadersDumpPath;
+            
+            if (NeedsOverride(nameof(GraphicsBackend)))
+                GraphicsBackend = configurationState.Graphics.GraphicsBackend;
+            
+            if (NeedsOverride(nameof(AntiAliasing)))
+                AntiAliasing = configurationState.Graphics.AntiAliasing;
+            
+            if (NeedsOverride(nameof(ScalingFilter)))
+                ScalingFilter = configurationState.Graphics.ScalingFilter;
+            
+            if (NeedsOverride(nameof(ScalingFilterLevel)))
+                ScalingFilterLevel = configurationState.Graphics.ScalingFilterLevel;
+
+            if (NeedsOverride(nameof(DramSize)))
+                DramSize = configurationState.System.DramSize;
+            
+            if (NeedsOverride(nameof(IgnoreMissingServices)))
+                IgnoreMissingServices = configurationState.System.IgnoreMissingServices;
+            
+            if (NeedsOverride(nameof(IgnoreControllerApplet)))
+                IgnoreControllerApplet = configurationState.IgnoreApplet;
+            
+            return;
+            
+            bool NeedsOverride(string argKey) => originalArgs.None(arg => arg.TrimStart('-').EqualsIgnoreCase(OptionName(argKey)));
+
+            string OptionName(string propertyName) =>
+                typeof(Options)!.GetProperty(propertyName)!.GetCustomAttribute<OptionAttribute>()!.LongName;
+        }
+        
         // General
+        
+        [Option("use-main-config", Required = false, Default = false, HelpText = "Use the settings from what was configured via the UI.")]
+        public bool InheritConfig { get; set; }
 
         [Option("root-data-dir", Required = false, HelpText = "Set the custom folder path for Ryujinx data.")]
         public string BaseDataDir { get; set; }
@@ -172,7 +327,7 @@ namespace Ryujinx.Headless.SDL2
         public bool LoggingDisableWarning { get; set; }
 
         [Option("disable-error-logs", Required = false, HelpText = "Disables printing error log messages.")]
-        public bool LoggingEnableError { get; set; }
+        public bool LoggingDisableError { get; set; }
 
         [Option("enable-trace-logs", Required = false, Default = false, HelpText = "Enables printing trace log messages.")]
         public bool LoggingEnableTrace { get; set; }

BIN
src/Ryujinx/Headless/Ryujinx.bmp


+ 1 - 1
src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs → src/Ryujinx/Headless/StatusUpdatedEventArgs.cs

@@ -1,6 +1,6 @@
 using System;
 
-namespace Ryujinx.Headless.SDL2
+namespace Ryujinx.Headless
 {
     class StatusUpdatedEventArgs(
         string vSyncMode,

+ 1 - 1
src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs → src/Ryujinx/Headless/Vulkan/VulkanWindow.cs

@@ -6,7 +6,7 @@ using System;
 using System.Runtime.InteropServices;
 using static SDL2.SDL;
 
-namespace Ryujinx.Headless.SDL2.Vulkan
+namespace Ryujinx.Headless
 {
     class VulkanWindow : WindowBase
     {

+ 4 - 3
src/Ryujinx.Headless.SDL2/WindowBase.cs → src/Ryujinx/Headless/WindowBase.cs

@@ -1,5 +1,6 @@
 using Humanizer;
 using LibHac.Tools.Fs;
+using Ryujinx.Ava;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
 using Ryujinx.Common.Logging;
@@ -26,7 +27,7 @@ using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
 using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
 using Switch = Ryujinx.HLE.Switch;
 
-namespace Ryujinx.Headless.SDL2
+namespace Ryujinx.Headless
 {
     abstract partial class WindowBase : IHostUIHandler, IDisposable
     {
@@ -136,7 +137,7 @@ namespace Ryujinx.Headless.SDL2
 
         private void SetWindowIcon()
         {
-            Stream iconStream = typeof(WindowBase).Assembly.GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
+            Stream iconStream = typeof(Program).Assembly.GetManifestResourceStream("HeadlessLogo");
             byte[] iconBytes = new byte[iconStream!.Length];
 
             if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
@@ -318,7 +319,7 @@ namespace Ryujinx.Headless.SDL2
                             Device.VSyncMode.ToString(),
                             dockedMode,
                             Device.Configuration.AspectRatio.ToText(),
-                            $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
+                            $"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
                             $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
                             $"GPU: {_gpuDriverName}"));
 

+ 12 - 5
src/Ryujinx/Program.cs

@@ -14,6 +14,7 @@ using Ryujinx.Common.GraphicsDriver;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.SystemInterop;
 using Ryujinx.Graphics.Vulkan.MoltenVK;
+using Ryujinx.Headless;
 using Ryujinx.SDL2.Common;
 using Ryujinx.UI.App.Common;
 using Ryujinx.UI.Common;
@@ -52,9 +53,15 @@ namespace Ryujinx.Ava
             }
 
             PreviewerDetached = true;
+            
+            if (args.Length > 0 && args[0] is "--no-gui" or "nogui")
+            {
+                HeadlessRyujinx.Entrypoint(args[1..]);
+                return 0;
+            }
 
             Initialize(args);
-
+            
             LoggerAdapter.Register();
 
             IconProvider.Current
@@ -106,7 +113,7 @@ namespace Ryujinx.Ava
             AppDomain.CurrentDomain.UnhandledException += (sender, e)
                 => ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating);
             AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit();
-
+            
             // Setup base data directory.
             AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
 
@@ -223,7 +230,7 @@ namespace Ryujinx.Ava
                 UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value;
         }
 
-        private static void PrintSystemInfo()
+        internal static void PrintSystemInfo()
         {
             Logger.Notice.Print(LogClass.Application, $"{RyujinxApp.FullAppName} Version: {Version}");
             SystemInfo.Gather().Print();
@@ -240,7 +247,7 @@ namespace Ryujinx.Ava
                     : $"Launch Mode: {AppDataManager.Mode}");
         }
 
-        private static void ProcessUnhandledException(object sender, Exception ex, bool isTerminating)
+        internal static void ProcessUnhandledException(object sender, Exception ex, bool isTerminating)
         {
             Logger.Log log = Logger.Error ?? Logger.Notice;
             string message = $"Unhandled exception caught: {ex}";
@@ -255,7 +262,7 @@ namespace Ryujinx.Ava
                 Exit();
         }
 
-        public static void Exit()
+        internal static void Exit()
         {
             DiscordIntegrationModule.Exit();
 

+ 3 - 1
src/Ryujinx/Ryujinx.csproj

@@ -47,6 +47,7 @@
     <PackageReference Include="Avalonia.Markup.Xaml.Loader" />
     <PackageReference Include="Avalonia.Svg" />
     <PackageReference Include="Avalonia.Svg.Skia" />
+    <PackageReference Include="CommandLineParser" />
     <PackageReference Include="DynamicData" />
     <PackageReference Include="FluentAvaloniaUI" />
     <PackageReference Include="Projektanker.Icons.Avalonia" />
@@ -66,6 +67,7 @@
   <ItemGroup>
     <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
     <ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
+    <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
     <ProjectReference Include="..\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj" />
     <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
     <ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
@@ -74,7 +76,6 @@
     <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
     <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
     <ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
-    <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
     <ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
     <ProjectReference Include="..\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj" />
     <ProjectReference Include="..\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
@@ -133,6 +134,7 @@
     <EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
     <EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
     <EmbeddedResource Include="Assets\Icons\Controller_ProCon.svg" />
+    <EmbeddedResource Include="Headless\Ryujinx.bmp" LogicalName="HeadlessLogo" />
   </ItemGroup>
   <ItemGroup>
     <AdditionalFiles Include="Assets\locales.json" />