diff --git a/src/Snowflake.Bootstrap.Windows/Program.cs b/src/Snowflake.Bootstrap.Windows/Program.cs index c88d3fd86..359b7cfd2 100644 --- a/src/Snowflake.Bootstrap.Windows/Program.cs +++ b/src/Snowflake.Bootstrap.Windows/Program.cs @@ -3,12 +3,27 @@ class Program { + private static SnowflakeShell snowflakeShell; + static void Main(string[] args) { Console.WriteLine("Starting Shell..."); - var snowflakeShell = new SnowflakeShell(); + AppDomain.CurrentDomain.ProcessExit += ExitHandler; + Program.snowflakeShell = new SnowflakeShell(); snowflakeShell.StartCore(); while (Console.ReadLine() != "exit") ; + + Console.WriteLine("Shutting down..."); snowflakeShell.ShutdownCore(); + + } + + private static void ExitHandler(object sender, EventArgs e) + { + Console.WriteLine("Shutting down due to force exit..."); + if (!snowflakeShell.IsShutdown) + { + snowflakeShell.ShutdownCore(); + } } } diff --git a/src/Snowflake.Bootstrap.Windows/Snowflake.Bootstrap.Windows.csproj b/src/Snowflake.Bootstrap.Windows/Snowflake.Bootstrap.Windows.csproj index 81ff566ce..5f882f919 100644 --- a/src/Snowflake.Bootstrap.Windows/Snowflake.Bootstrap.Windows.csproj +++ b/src/Snowflake.Bootstrap.Windows/Snowflake.Bootstrap.Windows.csproj @@ -3,7 +3,7 @@ Exe net6.0 - win-x64;win10-x64; + win-x64 diff --git a/src/Snowflake.Bootstrap.Windows/SnowflakeShell.cs b/src/Snowflake.Bootstrap.Windows/SnowflakeShell.cs index 21c718b6a..a990773f4 100644 --- a/src/Snowflake.Bootstrap.Windows/SnowflakeShell.cs +++ b/src/Snowflake.Bootstrap.Windows/SnowflakeShell.cs @@ -19,6 +19,8 @@ internal class SnowflakeShell private IServiceContainer loadedCore; + public bool IsShutdown { get; private set; } = false; + internal SnowflakeShell() { } @@ -35,12 +37,14 @@ public void RestartCore() { this.ShutdownCore(); this.StartCore(); + this.IsShutdown = false; } public void ShutdownCore() { this.loadedCore.Dispose(); GC.WaitForPendingFinalizers(); + this.IsShutdown = true; } } } diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Extensibility/IGameEmulation.cs b/src/Snowflake.Framework.Primitives/Orchestration/Extensibility/IGameEmulation.cs index 9fbc6a924..6d6cf5488 100644 --- a/src/Snowflake.Framework.Primitives/Orchestration/Extensibility/IGameEmulation.cs +++ b/src/Snowflake.Framework.Primitives/Orchestration/Extensibility/IGameEmulation.cs @@ -16,6 +16,11 @@ namespace Snowflake.Orchestration.Extensibility /// public interface IGameEmulation { + /// + /// A unique ID used to identify this emulation instance. + /// + Guid Guid { get; } + /// /// A list of that representes the input devices that will be used /// in this emulation instance. diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/CursorEventParams.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/CursorEventParams.cs new file mode 100644 index 000000000..dcc390008 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/CursorEventParams.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CursorEventParams + { + public Cursor Cursor; + } + + /// + /// Cursor types. These are the same as CefSharp cursors + /// + public enum Cursor : byte + { + Pointer = 0, + Cross, + Hand, + IBeam, + Wait, + Help, + EastResize, + NorthResize, + NortheastResize, + NorthwestResize, + SouthResize, + SoutheastResize, + SouthwestResize, + WestResize, + NorthSouthResize, + EastWestResize, + NortheastSouthwestResize, + NorthwestSoutheastResize, + ColumnResize, + RowResize, + MiddlePanning, + EastPanning, + NorthPanning, + NortheastPanning, + NorthwestPanning, + SouthPanning, + SoutheastPanning, + SouthwestPanning, + WestPanning, + Move, + VerticalText, + Cell, + ContextMenu, + Alias, + Progress, + NoDrop, + Copy, + None, + NotAllowed, + ZoomIn, + ZoomOut, + Grab, + Grabbing, + MiddlePanningVertical, + MiddlePanningHorizontal, + Custom, + DndNone, + DndMove, + DndCopy, + DndLink + } +} diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/GameWindowCommand.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/GameWindowCommand.cs new file mode 100644 index 000000000..1e41c3808 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/GameWindowCommand.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + + + [StructLayout(LayoutKind.Explicit, Pack = 1)] + public struct GameWindowCommand + { + public const byte GameWindowMagic = 0x9F; + + [FieldOffset(0)] public byte Magic; + [FieldOffset(1)] public GameWindowCommandType Type; + [FieldOffset(2)] public HandshakeEventParams HandshakeEvent; + [FieldOffset(2)] public WindowResizeEventParams ResizeEvent; + [FieldOffset(2)] public WindowMessageEventParams WindowMessageEvent; + [FieldOffset(2)] public MouseEventParams MouseEvent; + [FieldOffset(2)] public CursorEventParams CursorEvent; + [FieldOffset(2)] public OverlayTextureEventParams TextureEvent; + + public ReadOnlyMemory ToBuffer() + { + return StructUtils.ToMemory(this); + } + + public bool IntoBuffer(ref Span buffer) + { + return StructUtils.IntoSpan(this, ref buffer); + } + + public static GameWindowCommand? FromBuffer(ReadOnlyMemory buffer) + { + return StructUtils.FromSpan(buffer); + } + + public static GameWindowCommand Handshake(Guid id) + { + return new() + { + Magic = GameWindowMagic, + Type = GameWindowCommandType.Handshake, + HandshakeEvent = new() + { + Guid = id, + } + }; + } + + private static class StructUtils + { + public static unsafe ReadOnlyMemory ToMemory(T value) where T : unmanaged + { + byte* pointer = (byte*)&value; + + Memory _bytes = new byte[Marshal.SizeOf()]; + Span bytes = _bytes.Span; + + for (int i = 0; i < sizeof(T); i++) + { + bytes[i] = pointer[i]; + } + + return _bytes; + } + + public static unsafe bool IntoSpan(T value, ref Span bytes) where T : unmanaged + { + if (bytes.Length != Marshal.SizeOf()) + return false; + + byte* pointer = (byte*)&value; + for (int i = 0; i < sizeof(T); i++) + { + bytes[i] = pointer[i]; + } + return true; + } + + public static unsafe T? FromSpan(ReadOnlyMemory value) where T : unmanaged + { + if (value.Length != Marshal.SizeOf()) + { + Console.WriteLine("Expected size " + Marshal.SizeOf() + " but got " + value.Length); + return null; + } + + return MemoryMarshal.Cast(value.Span)[0]; + } + } + } +} diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/GameWindowCommandType.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/GameWindowCommandType.cs new file mode 100644 index 000000000..59b6ed562 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/GameWindowCommandType.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + public enum GameWindowCommandType : byte + { + Handshake = 1, + WindowResizeEvent = 2, + WindowMessageEvent = 3, + MouseEvent = 4, + CursorEvent = 5, + OverlayTextureEvent = 6, + ShutdownEvent = 7, + } +} diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/HandshakeEventParams.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/HandshakeEventParams.cs new file mode 100644 index 000000000..2c6658d40 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/HandshakeEventParams.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct HandshakeEventParams + { + public Guid Guid; + } +} diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/MouseEventParams.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/MouseEventParams.cs new file mode 100644 index 000000000..8be0680ba --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/MouseEventParams.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MouseEventParams + { + public MouseButton MouseDoubleClick; + + public MouseButton MouseDown; + public MouseButton MouseUp; + public ModifierKeys Modifiers; + + public float MouseX; + public float MouseY; + public float WheelX; + public float WheelY; + } + + [Flags] + public enum ModifierKeys : byte + { + None = 0, + Shift = 1 << 0, + Control = 1 << 1, + Alt = 1 << 2 + } + + [Flags] + public enum MouseButton : byte + { + None = 0, + Mouse1 = 1 << 0, + Mouse2 = 1 << 1, + Mouse3 = 1 << 2, + Mouse4 = 1 << 3, + Mouse5 = 1 << 4 + } +} diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/OverlayTextureEventParams.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/OverlayTextureEventParams.cs new file mode 100644 index 000000000..0865076f6 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/OverlayTextureEventParams.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct OverlayTextureEventParams + { + public nint TextureHandle; + public int SourceProcessId; + public uint Width; + public uint Height; + public ulong Size; + public ulong Alignment; + public nint SyncHandle; + } +} diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/WindowMessageEventParams.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/WindowMessageEventParams.cs new file mode 100644 index 000000000..c95b55ce8 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/WindowMessageEventParams.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct WindowMessageEventParams + { + public int Message; + public ulong WParam; + public int LParam; + } +} diff --git a/src/Snowflake.Framework.Primitives/Orchestration/Ingame/WindowResizeEventParams.cs b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/WindowResizeEventParams.cs new file mode 100644 index 000000000..8373ef574 --- /dev/null +++ b/src/Snowflake.Framework.Primitives/Orchestration/Ingame/WindowResizeEventParams.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Orchestration.Ingame +{ + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct WindowResizeEventParams + { + public int Height; + public int Width; + public byte Force; + } +} diff --git a/src/Snowflake.Framework.Primitives/Snowflake.Framework.Primitives.csproj b/src/Snowflake.Framework.Primitives/Snowflake.Framework.Primitives.csproj index 0137c0f63..bbf863052 100644 --- a/src/Snowflake.Framework.Primitives/Snowflake.Framework.Primitives.csproj +++ b/src/Snowflake.Framework.Primitives/Snowflake.Framework.Primitives.csproj @@ -6,6 +6,7 @@ 10.0 enable <_SnowflakeUseDevelopmentSDK>true + true diff --git a/src/Snowflake.Framework.Remoting.GraphQL/Model/Orchestration/GameEmulationType.cs b/src/Snowflake.Framework.Remoting.GraphQL/Model/Orchestration/GameEmulationType.cs index e0e3d88e7..0df874ed2 100644 --- a/src/Snowflake.Framework.Remoting.GraphQL/Model/Orchestration/GameEmulationType.cs +++ b/src/Snowflake.Framework.Remoting.GraphQL/Model/Orchestration/GameEmulationType.cs @@ -27,6 +27,9 @@ protected override void Configure(IObjectTypeDescriptor descript descriptor.Field(e => e.EmulationState) .Description("The current state of the emulation.") .Type>(); + descriptor.Field(e => e.Guid) + .Description("The GUID of the game emulation instance.") + .Type>(); } } } diff --git a/src/Snowflake.Framework.Remoting/Orchestration/IBrowserTab.cs b/src/Snowflake.Framework.Remoting/Orchestration/IBrowserTab.cs new file mode 100644 index 000000000..5ceeb68df --- /dev/null +++ b/src/Snowflake.Framework.Remoting/Orchestration/IBrowserTab.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Remoting.Orchestration +{ + public interface IBrowserTab : IDisposable + { + public void Navigate(Uri uri); + public Uri? CurrentLocation { get; } + public Task InitializeAsync() => this.InitializeAsync(new Uri("https://google.com")); + public Task InitializeAsync(Uri uri); + public NamedPipeClientStream GetCommandPipe(); + } +} diff --git a/src/Snowflake.Framework.Remoting/Orchestration/ICefBrowserService.cs b/src/Snowflake.Framework.Remoting/Orchestration/ICefBrowserService.cs new file mode 100644 index 000000000..3d47170f2 --- /dev/null +++ b/src/Snowflake.Framework.Remoting/Orchestration/ICefBrowserService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Remoting.Orchestration +{ + // todo move this to its own thing? + public interface ICefBrowserService + { + public Task InitializeAsync(); + public void Shutdown(); + public IBrowserTab GetTab(Guid tabId); + public void FreeTab(Guid tabId); + } +} diff --git a/src/Snowflake.Framework.Services/AssemblyLoader/AssemblyModuleLoader.cs b/src/Snowflake.Framework.Services/AssemblyLoader/AssemblyModuleLoader.cs index c36ab878a..f163ef9f1 100644 --- a/src/Snowflake.Framework.Services/AssemblyLoader/AssemblyModuleLoader.cs +++ b/src/Snowflake.Framework.Services/AssemblyLoader/AssemblyModuleLoader.cs @@ -44,7 +44,9 @@ public IEnumerable LoadModule(IModule module) cfg.LoggerTag = module.Entry.Replace(".dll", "").Replace("Snowflake.Support.", "SF.S."); // We need to load into the default context to allow accessing services exposed by other plugins. cfg.PreferSharedTypes = true; - cfg.LoadInMemory = true; + + // loading in memory makes some native-hosted Dlls act weird. + cfg.LoadInMemory = false; cfg.IsUnloadable = false; }); diff --git a/src/Snowflake.Framework.Tests.Input.Windows/InputTests.cs b/src/Snowflake.Framework.Tests.Input.Windows/InputTests.cs index dd317bba4..c5f10ef0b 100644 --- a/src/Snowflake.Framework.Tests.Input.Windows/InputTests.cs +++ b/src/Snowflake.Framework.Tests.Input.Windows/InputTests.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Management; using Snowflake.Support.InputEnumerators.Windows; +using System.Diagnostics; +using Reloaded.Injector; namespace Snowflake.Input.Tests.Windows { @@ -19,5 +21,22 @@ public void Test1() var e = new WindowsDeviceEnumerator(); var devices = e.QueryConnectedDevices().ToList(); } + + [Fact] + public void InjectRetroArch() + { + //E:\Emulators\yuzu + var startInfo = new ProcessStartInfo("E:\\Emulators\\RetroArch\\retroarch.exe"); + + //var startInfo = new ProcessStartInfo("E:\\Emulators\\yuzu\\yuzu.exe"); + + //startInfo.EnvironmentVariables.Add("VK_INSTANCE_LAYERS", "VK_LAYER_SABINOKAKU_injection"); + //startInfo.EnvironmentVariables.Add("ENABLE_SABINOKAKU_VULKAN", "1"); + var retroArchProcess = Process.Start(startInfo); + + var injector = new Injector(retroArchProcess); + Debugger.Break(); + injector.Inject(@"D:\coding\snowflake\src\Snowflake.Support.Orchestration.Overlay.Runtime.Windows\bin\Debug\net6.0\kaku-x64.dll"); + } } } diff --git a/src/Snowflake.Framework.Tests.Input.Windows/Snowflake.Framework.Tests.Input.Windows.csproj b/src/Snowflake.Framework.Tests.Input.Windows/Snowflake.Framework.Tests.Input.Windows.csproj index e8cd3f950..c803ba301 100644 --- a/src/Snowflake.Framework.Tests.Input.Windows/Snowflake.Framework.Tests.Input.Windows.csproj +++ b/src/Snowflake.Framework.Tests.Input.Windows/Snowflake.Framework.Tests.Input.Windows.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Snowflake.Framework/Orchestration/Extensibility/GameEmulation.cs b/src/Snowflake.Framework/Orchestration/Extensibility/GameEmulation.cs index e67dd5098..d5b8762a2 100644 --- a/src/Snowflake.Framework/Orchestration/Extensibility/GameEmulation.cs +++ b/src/Snowflake.Framework/Orchestration/Extensibility/GameEmulation.cs @@ -16,6 +16,7 @@ namespace Snowflake.Orchestration.Extensibility public abstract class GameEmulation : IAsyncDisposable, IGameEmulation { public IGame Game { get; } + public Guid Guid { get; } public IEnumerable ControllerPorts { get; } @@ -27,6 +28,7 @@ public GameEmulation(IGame game, this.Game = game; this.ControllerPorts = controllerPorts; this.SaveProfile = saveProfile; + this.Guid = Guid.NewGuid(); } public abstract Task SetupEnvironment(); @@ -48,7 +50,6 @@ public GameEmulation(IGame game, private bool IsDisposed { get; set; } = false; public GameEmulationState EmulationState { get; protected set; } = GameEmulationState.RequiresSetupEnvironment; - public async ValueTask DisposeAsync() { if (this.IsDisposed) return; diff --git a/src/Snowflake.Framework/Snowflake.Framework.csproj b/src/Snowflake.Framework/Snowflake.Framework.csproj index 083b74c62..6552fc5a8 100644 --- a/src/Snowflake.Framework/Snowflake.Framework.csproj +++ b/src/Snowflake.Framework/Snowflake.Framework.csproj @@ -7,6 +7,7 @@ 10.0 enable <_SnowflakeUseDevelopmentSDK>true + true diff --git a/src/Snowflake.Plugin.Emulators.RetroArch/Snowflake.Plugin.Emulators.RetroArch.csproj b/src/Snowflake.Plugin.Emulators.RetroArch/Snowflake.Plugin.Emulators.RetroArch.csproj index e51109e58..114840a89 100644 --- a/src/Snowflake.Plugin.Emulators.RetroArch/Snowflake.Plugin.Emulators.RetroArch.csproj +++ b/src/Snowflake.Plugin.Emulators.RetroArch/Snowflake.Plugin.Emulators.RetroArch.csproj @@ -4,8 +4,6 @@ net6.0 Snowflake <_SnowflakeUseDevelopmentSDK>true - true - $(BaseIntermediateOutputPath)Generated diff --git a/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Orchestration/OrchestrationMutations.cs b/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Orchestration/OrchestrationMutations.cs index 9c541bbe0..29ea7eace 100644 --- a/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Orchestration/OrchestrationMutations.cs +++ b/src/Snowflake.Support.GraphQL.FrameworkQueries/Mutations/Orchestration/OrchestrationMutations.cs @@ -87,12 +87,11 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .Build(); var instance = orchestrator.ProvisionEmulationInstance(game, controllers, input.CollectionID, save); - var guid = Guid.NewGuid(); - if (ctx.GetGameCache().TryAdd(guid, instance)) + if (ctx.GetGameCache().TryAdd(instance.Guid, instance)) return new EmulationInstancePayload() { GameEmulation = instance, - InstanceID = guid + InstanceID = instance.Guid }; return ErrorBuilder.New() .SetCode("ORCH_ERR_CREATE") diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/CefSharpBrowserService.cs b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/CefSharpBrowserService.cs new file mode 100644 index 000000000..91068dd37 --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/CefSharpBrowserService.cs @@ -0,0 +1,143 @@ +using CefSharp; +using CefSharp.OffScreen; +using Evergine.Bindings.RenderDoc; +using Silk.NET.Core.Native; +using Silk.NET.Direct3D11; +using Snowflake.Extensibility; +using Snowflake.Remoting.Orchestration; +using Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Remoting; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Browser +{ + internal class CefSharpBrowserService : ICefBrowserService, IDisposable + { + private bool disposedValue; + + public CefSharpBrowserService(ILogger logger, DirectoryInfo cachePath, RenderDoc renderDoc) + { + this.Logger = logger; + this.CacheDirectory = cachePath; + this.RenderDoc = renderDoc; + this.ShutdownEvent = new ManualResetEventSlim(); + this.StartEvent = new ManualResetEventSlim(); + this.InitializedEvent = new SemaphoreSlim(0, 1); + this.CefThread = new Thread(this.MainCefLoop); + this.Tabs = new ConcurrentDictionary(); + this.CefThread.Start(); + this.Device = new Direct3DDevice(); + } + + public Direct3DDevice Device { get; } + public ManualResetEventSlim StartEvent { get; } + public SemaphoreSlim InitializedEvent { get; } + + public ManualResetEventSlim ShutdownEvent { get; } + public Thread CefThread { get; } + public ILogger Logger { get; } + public DirectoryInfo CacheDirectory { get; } + public RenderDoc RenderDoc { get; } + + public ConcurrentDictionary Tabs; + + private bool Initialized { get; set; } + + public NamedPipeClientStream GetCommandPipe() + { + throw new NotImplementedException(); + } + + private void MainCefLoop() + { + this.Logger.Info("Entered CEF Loop thread, waiting for init."); + this.StartEvent.Wait(); + this.Logger.Info("CEF start event received."); + + CefSettings settings = new() + { + CachePath = this.CacheDirectory.FullName, + RemoteDebuggingPort = 10037, + // stop CEF from clogging up the console. + LogSeverity = LogSeverity.Fatal, + }; + Cef.EnableHighDPISupport(); + + settings.CefCommandLineArgs["autoplay-policy"] = "no-user-gesture-required"; + settings.SetOffScreenRenderingBestPerformanceArgs(); + settings.EnableAudio(); + Cef.Initialize(settings, true, browserProcessHandler: null); + this.Logger.Info("CEF started."); + this.InitializedEvent.Release(); + this.ShutdownEvent.Wait(); + this.Logger.Info("CEF shutting down..."); + Cef.Shutdown(); + this.Logger.Info("CEF shut down."); + } + + public async Task InitializeAsync() + { + if (this.Initialized) + return; + this.StartEvent.Set(); + await this.InitializedEvent.WaitAsync(); + this.Initialized = true; + } + + public void Shutdown() + { + this.ShutdownEvent.Set(); + foreach (var tab in this.Tabs) + { + tab.Value.Dispose(); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.Shutdown(); + this.Logger.Info("Waiting for CEF Thread to exit..."); + this.CefThread.Join(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public IBrowserTab GetTab(Guid tabId) + { + if (!this.Initialized) + throw new InvalidOperationException("Can not allocate a tab when service was not initialized."); + return this.Tabs.GetOrAdd(tabId, new CefSharpBrowserTab(this.Logger, tabId, this.Device, this.RenderDoc)); + + } + + public void FreeTab(Guid tabId) + { + if (this.Tabs.Remove(tabId, out var browserTab)) + { + browserTab.Dispose(); + } + } + } +} diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/CefSharpBrowserTab.cs b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/CefSharpBrowserTab.cs new file mode 100644 index 000000000..4b89cafb7 --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/CefSharpBrowserTab.cs @@ -0,0 +1,125 @@ +using CefSharp; +using CefSharp.OffScreen; +using Snowflake.Extensibility; +using Snowflake.Remoting.Orchestration; +using Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Remoting; +using System; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Silk.NET.Direct3D11; +using CefSharp.Structs; +using Snowflake.Orchestration.Ingame; +using Evergine.Bindings.RenderDoc; + +namespace Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Browser +{ + internal class CefSharpBrowserTab : IBrowserTab + { + private bool disposedValue; + + public CefSharpBrowserTab(ILogger logger, Guid tabGuid, Direct3DDevice device, Evergine.Bindings.RenderDoc.RenderDoc renderDoc) + { + this.Logger = logger; + this.TabGuid = tabGuid; + this.Device = device; + RenderDoc = renderDoc; + } + + private ChromiumWebBrowser Browser { get; set; } + private bool Initialized { get; set; } = false; + public Uri? CurrentLocation => this.Browser?.Address != null ? new Uri(this.Browser?.Address) : null; + public IngameCommandController CommandServer { get; private set; } + public ILogger Logger { get; } + public Guid TabGuid { get; } + public Direct3DDevice Device { get; } + public RenderDoc RenderDoc { get; } + private D3DSharedTextureRenderHandler Renderer { get; set; } + + public NamedPipeClientStream GetCommandPipe() + { + throw new NotImplementedException(); + } + + public async Task InitializeAsync(Uri uri) + { + if (this.Initialized || this.disposedValue) + return; + + this.CommandServer = new IngameCommandController(this.Logger, this.TabGuid); + this.CommandServer.Start(); + this.Renderer = new D3DSharedTextureRenderHandler(this.Device, this.CommandServer, this.RenderDoc); + this.Renderer.Resize(new(300, 300)); + this.Browser = new ChromiumWebBrowser(uri.AbsoluteUri); + + this.Browser.RenderHandler = this.Renderer; + this.CommandServer.CommandReceived += (cmd) => + { + switch (cmd.Type) + { + case GameWindowCommandType.WindowResizeEvent: + System.Drawing.Size size = new(Math.Max(1, cmd.ResizeEvent.Width), Math.Max(1, cmd.ResizeEvent.Height)); + this.Renderer.Resize(size, cmd.ResizeEvent.Force > 0); + this.Browser.Size = size; + this.Browser.GetBrowserHost().Invalidate(PaintElementType.View); + break; + case GameWindowCommandType.OverlayTextureEvent: + this.Browser.GetBrowserHost().Invalidate(PaintElementType.View); + break; + + } + }; + + WindowInfo windowInfo = new() + { + Width = 300, + Height = 300, + WindowlessRenderingEnabled = true, + }; + windowInfo.SetAsWindowless((nint)0); + + BrowserSettings browserSettings = new() + { + WindowlessFrameRate = 60, + }; + await this.Browser.CreateBrowserAsync(windowInfo, browserSettings); + await this.Browser.WaitForInitialLoadAsync(); + this.Browser.Size = new(300, 300); + + this.Initialized = true; + } + + public void Navigate(Uri uri) + { + if (uri.Equals(this.CurrentLocation)) + { + this.Browser?.Reload(); + return; + } + + this.Browser?.Load(uri.AbsoluteUri); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.Browser.Dispose(); + this.CommandServer.Stop(); + } + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/D3DSharedTextureRenderHandler.cs b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/D3DSharedTextureRenderHandler.cs new file mode 100644 index 000000000..1b4e67c83 --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/D3DSharedTextureRenderHandler.cs @@ -0,0 +1,324 @@ +using CefSharp; +using CefSharp.Enums; +using CefSharp.OffScreen; +using CefSharp.Structs; +using Evergine.Bindings.RenderDoc; +using Silk.NET.Core.Native; +using Silk.NET.Direct3D11; +using Silk.NET.DXGI; +using Snowflake.Orchestration.Ingame; +using Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Browser; +using Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Remoting; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Support.Orchestration.Overlay.Renderer.Windows +{ + internal class D3DSharedTextureRenderHandler : IRenderHandler + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe Guid* RiidOf(Guid @in) + { + return &@in; + } + + [DllImport("user32.dll")] + private static extern nint MonitorFromWindow(nint hwnd, uint dwFlags); + [DllImport("shcore.dll")] + private static extern void GetScaleFactorForMonitor(nint hMon, out uint pScale); + + [DllImport("kernel32.dll", SetLastError = true)] + [SuppressUnmanagedCodeSecurity] + private static extern bool CloseHandle(IntPtr hObject); + + // Texture paint stuff. + private ConcurrentQueue<(nint texturePointer, nint sharedHandle)> ObsoleteResources { get; } + + private unsafe ID3D11Texture2D* TargetTexture; + + private Texture2DDesc TargetTextureDescription; + public nint SharedTextureHandle { get; set; } + + // D3D device is plugin-wide. + private Direct3DDevice Device { get; } + private float DpiScaleFactor { get; } + private readonly object TextureLock = new(); + + // CEF buffers are 32-bit BGRA + private const byte CEFBufferBPP = 4; + private const uint INFINITE = 0xFFFFFFFF; + + // Command server to notify ppl + private IngameCommandController CommandServer { get; } + public RenderDoc RenderDoc { get; } + + public unsafe D3DSharedTextureRenderHandler(Direct3DDevice device, IngameCommandController commandServer, RenderDoc renderDoc) + { + this.CommandServer = commandServer; + RenderDoc = renderDoc; + this.ObsoleteResources = new ConcurrentQueue<(nint texturePointer, nint sharedHandle)>(); + // Todo: ask ingame for scale monitor handle. + nint hMon = MonitorFromWindow(0, 0x1); + GetScaleFactorForMonitor(hMon, out uint scale); + this.DpiScaleFactor = scale / 100f; + this.Device = device; + this.CommandServer.CommandReceived += CommandReceivedHandler; + } + + private void CommandReceivedHandler(GameWindowCommand command) + { + if (command.Type == GameWindowCommandType.OverlayTextureEvent) + this.CommandServer.Broadcast(new() + { + Magic = GameWindowCommand.GameWindowMagic, + Type = GameWindowCommandType.OverlayTextureEvent, + TextureEvent = new() + { + SourceProcessId = Environment.ProcessId, + TextureHandle = this.SharedTextureHandle, + Width = this.TargetTextureDescription.Width, + Height = this.TargetTextureDescription.Height, + Size = this.TargetTextureDescription.Width * this.TargetTextureDescription.Height * CEFBufferBPP * 2 + } + }); + } + + public ScreenInfo? GetScreenInfo() + { + return new() + { + DeviceScaleFactor = this.DpiScaleFactor + }; + } + + public bool GetScreenPoint(int viewX, int viewY, out int screenX, out int screenY) + { + screenX = viewX; + screenY = viewY; + + return false; + } + + public void Resize(System.Drawing.Size size, bool force = false) + { + Console.WriteLine("Resize buffer requested"); + if (!force && size.Height == this.TargetTextureDescription.Height && size.Width == this.TargetTextureDescription.Width) + { + Console.WriteLine("Resize would not change size, throttling."); + return; + } + + nint texPtr = this.Device.CreateNewCefTargetTexture(size); + unsafe + { + // Released when disposed in OnPaint. + ID3D11Texture2D* texture = (ID3D11Texture2D*)texPtr; + + // released on resize. + IDXGIResource1* texResrc = null; + lock (this.TextureLock) + { + nint oldTexture = (nint)this.TargetTexture; + nint oldHandle = this.SharedTextureHandle; + + texture->QueryInterface(RiidOf(IDXGIResource1.Guid), (void**)&texResrc); + + int res; + void* handle = null; + if ((res = texResrc->CreateSharedHandle(null, unchecked((uint)0x80000000ul), (char*)null, &handle)) != 0) + { + throw new InvalidOperationException($"Unable to update shared handled: {res}"); + } + + texture->GetDesc(ref this.TargetTextureDescription); + this.SharedTextureHandle = (nint)handle; + this.TargetTexture = texture; + this.ObsoleteResources.Enqueue((oldTexture, oldHandle)); + + // release resource + texResrc->Release(); + Console.WriteLine("updated buffer"); + } + } + + // Broadcast to all listeners to update their texture handle. + this.CommandServer.Broadcast(new() + { + Magic = GameWindowCommand.GameWindowMagic, + Type = GameWindowCommandType.OverlayTextureEvent, + TextureEvent = new() + { + TextureHandle = this.SharedTextureHandle, + SourceProcessId = Environment.ProcessId, + Width = this.TargetTextureDescription.Width, + Height = this.TargetTextureDescription.Height, + Size = this.TargetTextureDescription.Width * this.TargetTextureDescription.Height * CEFBufferBPP * 2, + } + }); + } + + public Rect GetViewRect() + { + // thanks browsingway. + static Rect DpiScaleRect(Rect rect, float scaleFactor) + { + return new Rect(rect.X, rect.Y, (int)Math.Ceiling(rect.Width * (1 / scaleFactor)), + (int)Math.Ceiling(rect.Height * (1 / scaleFactor))); + } + + // todo: scale dpi + return DpiScaleRect(new(0, 0, (int)this.TargetTextureDescription.Width, + (int)this.TargetTextureDescription.Height), this.DpiScaleFactor); + } + + public void OnCursorChange(IntPtr cursor, CursorType type, CursorInfo customCursorInfo) + { + this.CommandServer.Broadcast(new() + { + Magic = GameWindowCommand.GameWindowMagic, + Type = GameWindowCommandType.CursorEvent, + CursorEvent = new() { Cursor = (Cursor)(byte)type, } + }); + } + + public void OnPaint(PaintElementType type, Rect dirtyRect, IntPtr buffer, int width, int height) + { + // Don't care about popups. + if (type != PaintElementType.View) + return; + lock(this.TextureLock) + { + unsafe + { + // not initialized. + if (this.TargetTexture == null) + return; + } + + // thanks browsingway + int rowPitch = width * CEFBufferBPP; + int depthPitch = rowPitch * height; + + var texDesc = this.TargetTextureDescription; + Box destRegion = new() + { + Top = Math.Min(unchecked((uint)dirtyRect.Y), texDesc.Height), + Bottom = Math.Min(unchecked((uint)dirtyRect.Y) + unchecked((uint)dirtyRect.Height), texDesc.Height), + Left = Math.Min(unchecked((uint)dirtyRect.X), texDesc.Width), + Right = Math.Min(unchecked((uint)dirtyRect.X) + unchecked((uint)dirtyRect.Width), texDesc.Width), + Front = 0, + Back = 1 + }; + + unsafe + { + nint sourcePtr = buffer + (dirtyRect.X * CEFBufferBPP) + (dirtyRect.Y * rowPitch); + + ID3D11Device* device = null; + ID3D11DeviceContext* context = null; + ID3D11Resource* textureResc = null; + // this is going to be a pain isnt it. + IDXGIKeyedMutex* textureMtx = null; + + this.TargetTexture->QueryInterface(RiidOf(ID3D11Resource.Guid), (void**)&textureResc); + this.TargetTexture->QueryInterface(RiidOf(IDXGIKeyedMutex.Guid), (void**)&textureMtx); + + this.TargetTexture->GetDevice(ref device); + device->GetImmediateContext(ref context); + //this.RenderDoc.API.StartFrameCapture((nint)context, (IntPtr)null); + + textureMtx->AcquireSync(0, INFINITE); // infinite + context->UpdateSubresource(textureResc, 0, ref destRegion, (void*)sourcePtr, (uint)rowPitch, (uint)depthPitch); + context->Flush(); + textureMtx->ReleaseSync(0); + + //this.RenderDoc.API.EndFrameCapture((nint)context, (IntPtr)null); + + + // ensure we release all local COM pointers here. + // really wish C# had traits + textureResc->Release(); + textureMtx->Release(); + context->Release(); + device->Release(); + } + + // cleanup.. + while (this.ObsoleteResources.TryDequeue(out (nint texturePointer, nint sharedHandle) texture)) + { + unsafe + { + // Honestly should check error but if this fails we just leak it. + // Expectation is that Resize is always client-side triggered anyways, + // so if their handle is properly duped this doesn't matter. + CloseHandle(texture.sharedHandle); + + // Texture will _actually_ be freeded when all clients acually let go of it. + if (texture.texturePointer != 0) + ((ID3D11Texture2D*)texture.texturePointer)->Release(); + } + } + } + } + + public void Dispose() + { + // todo... + + return; + } + + #region Not Supported + public void OnAcceleratedPaint(PaintElementType type, Rect dirtyRect, IntPtr sharedHandle) + { + // Not supported. + return; + } + + public void OnImeCompositionRangeChanged(CefSharp.Structs.Range selectedRange, Rect[] characterBounds) + { + // Not supported. + return; + } + + + public void OnPopupShow(bool show) + { + // Not supported. + return; + } + + public void OnPopupSize(Rect rect) + { + // Not supported. + return; + } + + public void OnVirtualKeyboardRequested(IBrowser browser, TextInputMode inputMode) + { + // Not supported. + return; + } + + public bool StartDragging(IDragData dragData, DragOperationsMask mask, int x, int y) + { + // not supported. + return false; + } + + public void UpdateDragCursor(DragOperationsMask operation) + { + return; + } + #endregion + } +} diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/Direct3DDevice.cs b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/Direct3DDevice.cs new file mode 100644 index 000000000..1dc353fd9 --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Browser/Direct3DDevice.cs @@ -0,0 +1,118 @@ +using Silk.NET.Core.Native; +using Silk.NET.Direct3D11; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Browser +{ + internal class Direct3DDevice : IDisposable + { + private D3D11 Direct3D; + private unsafe ID3D11Device* RenderDevice; + private unsafe ID3D11DeviceContext* RenderContext; + private bool disposedValue; + private static readonly D3DFeatureLevel[] FEATURE_LEVELS = new[] { D3DFeatureLevel.D3DFeatureLevel111 }; + + public Direct3DDevice() + { + this.Direct3D = D3D11.GetApi(); + unsafe + { + Span requestFeatureLevels = FEATURE_LEVELS.AsSpan(); + D3DFeatureLevel outFeatureLevel = 0; + + // Released on dispose. + ID3D11Device* device = null; + ID3D11DeviceContext* context = null; + + int? result = 0; + fixed (D3DFeatureLevel* featureLevels = requestFeatureLevels) + { + result = Direct3D.CreateDevice( + null, + D3DDriverType.D3DDriverTypeHardware, + 0, + (uint)(CreateDeviceFlag.CreateDeviceBgraSupport|CreateDeviceFlag.CreateDeviceDebug), + featureLevels, + (uint)requestFeatureLevels.Length, + D3D11.SdkVersion, + ref device, + ref outFeatureLevel, + ref context + ); + } + + if (result == null || result.Value < 0) + { + throw new PlatformNotSupportedException("Direct3D11 not supported."); + } + + this.RenderDevice = device; + this.RenderContext = context; + } + } + + /// + /// Creates a target texture to paint CEF with the given size. + /// + /// + /// + /// + public nint CreateNewCefTargetTexture(Size size) + { + Texture2DDesc texture2DDesc = new() + { + Width = (uint)size.Width, + Height = (uint)size.Height, + MipLevels = 1, + ArraySize = 1, + Format = Silk.NET.DXGI.Format.FormatB8G8R8A8Unorm, + SampleDesc = new(1,0), + Usage = Usage.UsageDefault, + BindFlags = (uint)(BindFlag.BindShaderResource), + // needs NT Handle for DX12/OGL/vK interop + MiscFlags = (uint)(ResourceMiscFlag.ResourceMiscSharedNthandle | ResourceMiscFlag.ResourceMiscSharedKeyedmutex), + CPUAccessFlags = 0, + }; + + unsafe + { + ID3D11Texture2D* texture = null; + int result = 0; + if ((result = this.RenderDevice->CreateTexture2D(ref texture2DDesc, null, ref texture)) != 0) + { + throw new InvalidOperationException($"Failed to create D3D11 Texture: {result}"); + } + return (nint)texture; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + unsafe + { + this.RenderDevice->Release(); + this.RenderContext->Release(); + } + } + disposedValue = true; + } + } + + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/CefRendererComposable.cs b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/CefRendererComposable.cs new file mode 100644 index 000000000..f5ce0142a --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/CefRendererComposable.cs @@ -0,0 +1,35 @@ +using Evergine.Bindings.RenderDoc; +using Snowflake.Extensibility; +using Snowflake.Loader; +using Snowflake.Remoting.Orchestration; +using Snowflake.Services; +using Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Browser; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Snowflake.Support.Orchestration.Overlay.Renderer.Windows +{ + public class CefRendererComposable : IComposable + { + [ImportService(typeof(IServiceRegistrationProvider))] + [ImportService(typeof(ILogProvider))] + public void Compose(IModule composableModule, Loader.IServiceRepository serviceContainer) + { + + var logger = serviceContainer.Get(); + var services = serviceContainer.Get(); + + var cachePath = composableModule.ContentsDirectory.CreateSubdirectory("cache"); + RenderDoc.Load(out var rd); + var browser = new CefSharpBrowserService(logger.GetLogger("cefsharp"), cachePath, rd); + services.RegisterService(browser); + Task.Run(async () => { + await browser.InitializeAsync(); + // todo: this should be done by the emulator orchestrator, but for debug purposes we'll do one empty. + var tab = browser.GetTab(Guid.Empty); + await tab.InitializeAsync(); + }); + } + } +} \ No newline at end of file diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Remoting/IngameCommandController.cs b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Remoting/IngameCommandController.cs new file mode 100644 index 000000000..f18257f0b --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Remoting/IngameCommandController.cs @@ -0,0 +1,189 @@ +using Snowflake.Extensibility; +using Snowflake.Orchestration.Ingame; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Snowflake.Support.Orchestration.Overlay.Renderer.Windows.Remoting +{ + internal class IngameCommandController + { + private ILogger Logger { get; } + CancellationTokenSource TokenSource { get; } + Thread WatchdogThread { get; set; } + ConcurrentQueue OpenPipes { get; set; } + + public delegate void IngameCommandHandler(GameWindowCommand command); + public event IngameCommandHandler CommandReceived; + + private Guid InstanceGuid { get; } + public IngameCommandController(ILogger logger, Guid instanceGuid) + { + this.InstanceGuid = instanceGuid; + this.Logger = logger; + this.TokenSource = new(); + this.OpenPipes = new(); + } + + public void Start() + { + this.WatchdogThread = new Thread(async (data) => await ServerThread((CancellationToken)data)); + this.WatchdogThread.Start(this.TokenSource.Token); + } + + public void Stop() + { + this.TokenSource.Cancel(); + } + + public NamedPipeClientStream OpenNew() + { + return new NamedPipeClientStream("Snowflake.Orchestration.Renderer-"+this.InstanceGuid.ToString("N")); + } + + public void Broadcast(GameWindowCommand command) + { + Task.Run(async () => await this.BroadcastAsync(command)).ConfigureAwait(false); + } + + public async Task BroadcastAsync(GameWindowCommand command) + { + List tempStreams = new(); + + this.Logger.Info($"Broadcast {command.Type}, {this.OpenPipes.Count} clients connected."); + + while (this.OpenPipes.TryDequeue(out var pipe)) + { + if (!pipe.IsConnected) + { + this.Logger.Info($"Disposing stale client."); + await pipe.DisposeAsync(); // goodbye pipe + continue; + } + this.Logger.Info($"Broadcasting {command.Type}"); + await pipe.WriteAsync(command.ToBuffer(), this.TokenSource.Token); + tempStreams.Add(pipe); + } + + // add pipes back to working set. + foreach (var pipe in tempStreams) + { + this.OpenPipes.Enqueue(pipe); + } + } + + public async Task ServerWorkThread(NamedPipeServerStream pipeServer, CancellationToken shutdownEvent) + { + // todo: handle broken pipe + Memory readBuffer = new byte[Marshal.SizeOf()]; + try + { + while (!shutdownEvent.IsCancellationRequested && pipeServer.IsConnected) + { + this.Logger.Info("Read loop."); + int bytesRead = await pipeServer.ReadAsync(readBuffer, shutdownEvent); + if (shutdownEvent.IsCancellationRequested) + { + this.Logger.Info("Cancellation requested."); + break; + } + + if (bytesRead == 0) + { + this.Logger.Info("Pipe closed"); + break; + } + if (readBuffer.Span[0] != GameWindowCommand.GameWindowMagic) + { + this.Logger.Info("Unexpected magic number: " + readBuffer.Span[0]); + continue; + } + + if (bytesRead != readBuffer.Length) + { + this.Logger.Info($"Unexpected length {bytesRead}, expected {readBuffer.Length}"); + continue; + } + + GameWindowCommand? commandBytes = GameWindowCommand.FromBuffer(readBuffer); + if (!commandBytes.HasValue) + { + this.Logger.Info($"Unexpected payload."); + continue; + } + var command = commandBytes.Value; + + switch (command.Type) + { + case GameWindowCommandType.Handshake: + this.Logger.Info("Got handshake pong command in cmdthread " + this.InstanceGuid); + this.Logger.Info("Sending pong with " + command.HandshakeEvent.Guid.ToString("N")); + var buffer = command.ToBuffer(); + await pipeServer.WriteAsync(buffer, shutdownEvent); + break; + case GameWindowCommandType.ShutdownEvent: + this.Logger.Info("cmdthread shutdown request for " + this.InstanceGuid); + this.TokenSource.Cancel(); + break; + case GameWindowCommandType.WindowResizeEvent: + case GameWindowCommandType.WindowMessageEvent: + case GameWindowCommandType.OverlayTextureEvent: + case GameWindowCommandType.MouseEvent: + case GameWindowCommandType.CursorEvent: + this.CommandReceived?.Invoke(command); + break; + } + } + } + catch(Exception e) + { + this.Logger.Info("client pipe broken for " + this.InstanceGuid + $" because \n {e}"); + } + finally + { + pipeServer.Dispose(); + this.Logger.Info("client connection closed " + this.InstanceGuid); + } + } + + public async Task ServerThread(CancellationToken shutdownEvent) + { + this.Logger.Info("Started ingame overlay IPC server."); + // todo proper cancellation. + while(!shutdownEvent.IsCancellationRequested) + { + var pipeServer = new NamedPipeServerStream( + "Snowflake.Orchestration.Renderer-" + this.InstanceGuid.ToString("N"), + PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + try + { + await pipeServer.WaitForConnectionAsync(shutdownEvent); + if (shutdownEvent.IsCancellationRequested) + break; + this.Logger.Info("Connection established to new client, shunting to handler thread."); + this.OpenPipes.Enqueue(pipeServer); + // hand off ownership of pipe to handler thread. + new Thread(async (data) => + { + (NamedPipeServerStream pipeServer, CancellationToken shutdownEvent) = + ((NamedPipeServerStream, CancellationToken))data; + await this.ServerWorkThread(pipeServer, shutdownEvent); + }).Start((pipeServer, shutdownEvent)); + } + + catch(Exception e) + { + Console.WriteLine(e); + continue; + } + } + } + } +} diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Snowflake.Support.Orchestration.Overlay.Renderer.Windows.csproj b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Snowflake.Support.Orchestration.Overlay.Renderer.Windows.csproj new file mode 100644 index 000000000..30d8c81f7 --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/Snowflake.Support.Orchestration.Overlay.Renderer.Windows.csproj @@ -0,0 +1,14 @@ + + + net6.0 + <_SnowflakeUseDevelopmentSDK>true + win-x64 + true + + + + + + + + diff --git a/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/module.json b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/module.json new file mode 100644 index 000000000..485c1f80a --- /dev/null +++ b/src/Snowflake.Support.Orchestration.Overlay.Renderer.Windows/module.json @@ -0,0 +1,8 @@ +{ + "entry": "Snowflake.Support.Orchestration.Overlay.Renderer.Windows.dll", + "loader": "assembly", + "frameworkVersion": "1.0.0", + "version": "1.0.0", + "author": "Snowflake", + "name": "Snowflake CefSharp Renderer" +} \ No newline at end of file diff --git a/src/Snowflake.sln b/src/Snowflake.sln index 45b27f3c9..5efab50d5 100644 --- a/src/Snowflake.sln +++ b/src/Snowflake.sln @@ -96,6 +96,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snowflake.Framework.Languag EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snowflake.Plugin.Scraping.Filename", "Snowflake.Plugin.Scraping.Filename\Snowflake.Plugin.Scraping.Filename.csproj", "{09F314C3-054A-4907-96A4-C180976FA4DD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snowflake.Support.Orchestration.Overlay.Renderer.Windows", "Snowflake.Support.Orchestration.Overlay.Renderer.Windows\Snowflake.Support.Orchestration.Overlay.Renderer.Windows.csproj", "{BCE0A187-9D45-41C7-8C6F-D0AE20D97866}" +EndProject +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -902,6 +905,30 @@ Global {09F314C3-054A-4907-96A4-C180976FA4DD}.Release-Module|x64.Build.0 = Release-Module|Any CPU {09F314C3-054A-4907-96A4-C180976FA4DD}.Release-Module|x86.ActiveCfg = Release-Module|Any CPU {09F314C3-054A-4907-96A4-C180976FA4DD}.Release-Module|x86.Build.0 = Release-Module|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug|x64.ActiveCfg = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug|x64.Build.0 = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug|x86.ActiveCfg = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug|x86.Build.0 = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug-Module|Any CPU.ActiveCfg = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug-Module|Any CPU.Build.0 = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug-Module|x64.ActiveCfg = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug-Module|x64.Build.0 = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug-Module|x86.ActiveCfg = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Debug-Module|x86.Build.0 = Debug|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release|Any CPU.Build.0 = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release|x64.ActiveCfg = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release|x64.Build.0 = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release|x86.ActiveCfg = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release|x86.Build.0 = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release-Module|Any CPU.ActiveCfg = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release-Module|Any CPU.Build.0 = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release-Module|x64.ActiveCfg = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release-Module|x64.Build.0 = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release-Module|x86.ActiveCfg = Release|Any CPU + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866}.Release-Module|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -944,6 +971,7 @@ Global {22986263-0BAD-4D78-9EE4-641F6534B050} = {FBD15674-246A-4C57-B865-07214D8BB9A1} {966B61B4-183E-40DE-8E47-40BEBD117638} = {49B7A61F-B3BE-485E-84D0-B78C08656412} {09F314C3-054A-4907-96A4-C180976FA4DD} = {86D5767F-B32D-4E1B-BBA2-B87A0DEC6C6F} + {BCE0A187-9D45-41C7-8C6F-D0AE20D97866} = {FBD15674-246A-4C57-B865-07214D8BB9A1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {532F5D25-2D82-45B3-BF60-DDA40A0FB795}