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}