Add initial CEF integration to engine.

This commit is contained in:
Vera Aguilera Puerto
2021-07-02 23:31:18 +02:00
parent 5aa950e7f7
commit c85bb81606
8 changed files with 657 additions and 0 deletions

3
.gitmodules vendored
View File

@@ -16,3 +16,6 @@
[submodule "Linguini"]
path = Linguini
url = https://github.com/space-wizards/Linguini
[submodule "cefglue"]
path = cefglue
url = https://gitlab.com/xiliumhq/chromiumembedded/cefglue/

View File

@@ -0,0 +1,126 @@
// Copyright © 2018 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using Robust.Shared.Log;
using Xilium.CefGlue;
namespace Robust.Client.CEF
{
/// <summary>
/// BitmapBuffer contains a byte[] used to store the Bitmap generated from <see cref="IRenderHandler.OnPaint"/>
/// and associated methods for updating that buffer and creating a <see cref="Bitmap"/> from the actaual Buffer
/// </summary>
internal class BitmapBuffer
{
private const int BytesPerPixel = 4;
private const PixelFormat Format = PixelFormat.Format32bppPArgb;
private byte[] buffer = Array.Empty<byte>();
/// <summary>
/// Number of bytes
/// </summary>
public int NumberOfBytes { get; private set; }
/// <summary>
/// Width
/// </summary>
public int Width { get; private set; }
/// <summary>
/// Height
/// </summary>
public int Height { get; private set; }
/// <summary>
/// Dirty Rect - unified region containing th
/// </summary>
public CefRectangle DirtyRect { get; private set; }
/// <summary>
/// Locking object used to syncronise access to the underlying buffer
/// </summary>
public object BitmapLock { get; private set; }
/// <summary>
/// Create a new instance of BitmapBuffer
/// </summary>
/// <param name="bitmapLock">Reference to the bitmapLock, a shared lock object is expected</param>
internal BitmapBuffer(object bitmapLock)
{
BitmapLock = bitmapLock;
}
/// <summary>
/// Get the byte[] array that represents the Bitmap
/// </summary>
public byte[] Buffer
{
get { return buffer; }
}
//TODO: May need to Pin the buffer in memory using GCHandle.Alloc(this.buffer, GCHandleType.Pinned);
private void ResizeBuffer(int width, int height)
{
if (buffer == null || width != Width || height != Height)
{
//No of Pixels (width * height) * BytesPerPixel
NumberOfBytes = width * height * BytesPerPixel;
buffer = new byte[NumberOfBytes];
Width = width;
Height = height;
}
}
/// <summary>
/// Copy data from the unmanaged buffer (IntPtr) into our managed buffer.
/// Locks BitmapLock before performing any update
/// </summary>
/// <param name="width">width</param>
/// <param name="height">height</param>
/// <param name="buffer">pointer to unmanaged buffer (void*)</param>
/// <param name="dirtyRect">rectangle to be updated</param>
public void UpdateBuffer(int width, int height, IntPtr buffer, CefRectangle dirtyRect)
{
lock (BitmapLock)
{
DirtyRect = dirtyRect;
ResizeBuffer(width, height);
Marshal.Copy(buffer, this.buffer, 0, NumberOfBytes);
}
}
/// <summary>
/// Creates a new Bitmap given with the current Width/Height and <see cref="Format"/>
/// then copies the buffer that represents the bitmap.
/// Locks <see cref="BitmapLock"/> before creating the <see cref="Bitmap"/>
/// </summary>
/// <returns>A new bitmap</returns>
public Bitmap? CreateBitmap()
{
lock (BitmapLock)
{
if (Width == 0 || Height == 0 || buffer.Length == 0)
{
return null;
}
var bitmap = new Bitmap(Width, Height, Format);
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, Format);
Marshal.Copy(Buffer, 0, bitmapData.Scan0, NumberOfBytes);
bitmap.UnlockBits(bitmapData);
return bitmap;
}
}
}
}

View File

@@ -0,0 +1,254 @@
using System;
using System.Drawing.Imaging;
using System.IO;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Xilium.CefGlue;
namespace Robust.Client.CEF
{
// Funny browser control to integrate in UI.
public class BrowserControl : Control
{
[Dependency] private readonly IClyde _clyde = default!;
private RobustWebClient _client;
private CefBrowser _browser;
private ControlRenderHandler _renderer;
// TODO CEF: I don't know how to UI, are these methods below right?
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var buffer = _renderer.Buffer;
return new Vector2(buffer.Width, buffer.Height);
}
protected override Vector2 MeasureCore(Vector2 availableSize)
{
var buffer = _renderer.Buffer;
return new Vector2(buffer.Width, buffer.Height);
}
public BrowserControl()
{
IoCManager.InjectDependencies(this);
// A funny render handler that will allow us to render to the control.
_renderer = new ControlRenderHandler(this);
// A funny web cef client. This can actually be shared by multiple browsers, but I'm not sure how the
// rendering would work in that case? TODO CEF: Investigate a way to share the web client?
_client = new RobustWebClient(_renderer);
var info = CefWindowInfo.Create();
// FUNFACT: If you DO NOT set these below and set info.Width/info.Height instead, you get an external window
// Good to know, huh? Setup is the same, except you can pass a dummy render handler to the CEF client.
info.SetAsWindowless(IntPtr.Zero, false); // TODO CEF: Pass parent handle?
info.WindowlessRenderingEnabled = true;
var settings = new CefBrowserSettings()
{
WindowlessFrameRate = 60,
};
// Create the web browser! And by default, we go to about:blank.
_browser = CefBrowserHost.CreateBrowserSync(info, _client, settings, "about:blank");
}
protected override void MouseMove(GUIMouseMoveEventArgs args)
{
base.MouseMove(args);
// TODO CEF: Modifiers
_browser.GetHost().SendMouseMoveEvent(new CefMouseEvent((int)args.RelativePosition.X, (int)args.RelativePosition.Y, CefEventFlags.None), false);
}
protected override void MouseExited()
{
base.MouseExited();
// TODO CEF: Modifiers
_browser.GetHost().SendMouseMoveEvent(new CefMouseEvent(0, 0, CefEventFlags.None), true);
}
protected override void MouseWheel(GUIMouseWheelEventArgs args)
{
base.MouseWheel(args);
// TODO CEF: Modifiers
_browser.GetHost().SendMouseWheelEvent(new CefMouseEvent((int)args.RelativePosition.X, (int)args.RelativePosition.Y, CefEventFlags.None), (int)args.Delta.X*4, (int)args.Delta.Y*4);
}
protected override void TextEntered(GUITextEventArgs args)
{
base.TextEntered(args);
// TODO CEF: Yeah the thing below is not how this works.
// _browser.GetHost().SendKeyEvent(new CefKeyEvent(){NativeKeyCode = (int) args.CodePoint});
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
// TODO CEF Clean up this shitty code. Also add middle click.
if (args.Function == EngineKeyFunctions.UIClick)
{
_browser.GetHost().SendMouseClickEvent(new CefMouseEvent((int)args.RelativePosition.X, (int)args.RelativePosition.Y, CefEventFlags.None), CefMouseButtonType.Left, true, 1);
} else if (args.Function == EngineKeyFunctions.UIRightClick)
{
_browser.GetHost().SendMouseClickEvent(new CefMouseEvent((int)args.RelativePosition.X, (int)args.RelativePosition.Y, CefEventFlags.None), CefMouseButtonType.Middle, true, 1);
}
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
// TODO CEF Clean up this shitty code. Also add middle click.
if (args.Function == EngineKeyFunctions.UIClick)
{
_browser.GetHost().SendMouseClickEvent(new CefMouseEvent((int)args.RelativePosition.X, (int)args.RelativePosition.Y, CefEventFlags.None), CefMouseButtonType.Left, false, 1);
} else if (args.Function == EngineKeyFunctions.UIRightClick)
{
_browser.GetHost().SendMouseClickEvent(new CefMouseEvent((int)args.RelativePosition.X, (int)args.RelativePosition.Y, CefEventFlags.None), CefMouseButtonType.Middle, false, 1);
}
}
protected override void Resized()
{
base.Resized();
_browser.GetHost().NotifyMoveOrResizeStarted();
_browser.GetHost().WasResized();
}
public void Browse(string url)
{
_browser.GetMainFrame().LoadUrl(url);
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
var bitmap = _renderer.Buffer.CreateBitmap();
if (bitmap == null)
return;
using var memoryStream = new MemoryStream();
// Oof, ow, owie the allocations.
bitmap.Save(memoryStream, ImageFormat.Png);
memoryStream.Seek(0, SeekOrigin.Begin);
// TODO CEF: There must certainly be a better way of doing this... Right?
var texture = _clyde.LoadTextureFromPNGStream(memoryStream);
handle.DrawTexture(texture, Vector2.Zero);
}
}
internal class ControlRenderHandler : CefRenderHandler
{
public BitmapBuffer Buffer { get; }
private Control _control;
internal ControlRenderHandler(Control control)
{
Buffer = new BitmapBuffer(this);
_control = control;
}
protected override CefAccessibilityHandler GetAccessibilityHandler()
{
if (_control.Disposed)
return null!;
// TODO CEF: Do we need this? Can we return null instead?
return new AccessibilityHandler();
}
protected override void GetViewRect(CefBrowser browser, out CefRectangle rect)
{
if (_control.Disposed)
{
rect = new CefRectangle();
return;
}
// TODO CEF: Do we need to pass real screen coords? Cause what we do already works...
//var screenCoords = _control.ScreenCoordinates;
//rect = new CefRectangle((int) screenCoords.X, (int) screenCoords.Y, (int)Math.Max(_control.Size.X, 1), (int)Math.Max(_control.Size.Y, 1));
// We do the max between size and 1 because it will LITERALLY CRASH WITHOUT AN ERROR otherwise.
rect = new CefRectangle(0, 0, (int)Math.Max(_control.Size.X, 1), (int)Math.Max(_control.Size.Y, 1));
}
protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
{
if (_control.Disposed)
return false;
// TODO CEF: Get actual scale factor?
screenInfo.DeviceScaleFactor = 1.0f;
return true;
}
protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
{
if (_control.Disposed)
return;
}
protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects, IntPtr buffer, int width, int height)
{
if (_control.Disposed)
return;
foreach (var dirtyRect in dirtyRects)
{
Buffer.UpdateBuffer(width, height, buffer, dirtyRect);
}
}
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects, IntPtr sharedHandle)
{
// Unused, but we're forced to implement it so.. NOOP.
}
protected override void OnScrollOffsetChanged(CefBrowser browser, double x, double y)
{
if (_control.Disposed)
return;
}
protected override void OnImeCompositionRangeChanged(CefBrowser browser, CefRange selectedRange, CefRectangle[] characterBounds)
{
if (_control.Disposed)
return;
}
// TODO CEF: Do we need this?
private class AccessibilityHandler : CefAccessibilityHandler
{
protected override void OnAccessibilityTreeChange(CefValue value)
{
}
protected override void OnAccessibilityLocationChange(CefValue value)
{
}
}
}
}

View File

@@ -0,0 +1,182 @@
using System;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
// The library we're using right now. TODO CEF: Do we want to use something else? We will need to ship it ourselves if so.
using Xilium.CefGlue;
namespace Robust.Client.CEF
{
// Register this with IoC.
// TODO CEF: think if making this inherit CefApp is a good idea...
// TODO CEF: A way to handle external window browsers...
[UsedImplicitly]
public class CefManager : CefApp, IPostInjectInit
{
[Dependency] private readonly IConsoleHost _consoleHost = default!;
private readonly BrowserProcessHandler _browserProcessHandler;
private readonly RenderProcessHandler _renderProcessHandler;
private bool _initialized = false;
public CefManager()
{
// Probably not needed?
_renderProcessHandler = new RenderProcessHandler();
_browserProcessHandler = new BrowserProcessHandler();
}
/// <summary>
/// Call this to initialize CEF.
/// </summary>
public void Initialize()
{
DebugTools.Assert(!_initialized);
// Register this funny command for easy debugging.
// TODO CEF: Actually make this command work. I used to have this in content before because it was easier...
_consoleHost.RegisterCommand("browse", "Opens an embedded web browser in an in-game window!", "browse <url>", BrowseCommand);
var settings = new CefSettings()
{
WindowlessRenderingEnabled = true, // So we can render to our UI controls.
ExternalMessagePump = false, // Unsure, honestly. TODO CEF: Research this?
NoSandbox = true, // Not disabling the sandbox crashes CEF.
// TODO CEF Unhardcode these paths below somehow, it seems CEF needs paths in the actual disk for these...
// Multi-process currently doesn't work...
BrowserSubprocessPath = "/home/zumo/Projects/space-station-14/bin/Content.Client/Robust.Client.CEF",
// I don't think this is needed? Research.
LocalesDirPath = "/home/zumo/Projects/space-station-14/bin/Content.Client/locales/",
// I don't think this is needed either? Do research.
ResourcesDirPath = "/home/zumo/Projects/space-station-14/bin/Content.Client/",
};
Logger.Info($"CEF Version: {CefRuntime.ChromeVersion}");
// --------------------------- README --------------------------------------------------
// By the way! You're gonna need the CEF binaries in your client's bin folder.
// More specifically, version cef_binary_91.1.21+g9dd45fe+chromium-91.0.4472.114
// https://cef-builds.spotifycdn.com/cef_binary_91.1.21%2Bg9dd45fe%2Bchromium-91.0.4472.114_windows64_minimal.tar.bz2
// https://cef-builds.spotifycdn.com/cef_binary_91.1.21%2Bg9dd45fe%2Bchromium-91.0.4472.114_linux64_minimal.tar.bz2
// Here's how to get it to work:
// 1. Copy all the contents of "Release" to the bin folder.
// 2. Copy all the contents of "Resources" to the bin folder.
// Supposedly, you should just need libcef.so in Release and icudtl.dat in Resources...
// The rest might be optional.
// Maybe. Good luck! If you get odd crashes with no info and a weird exit code, use GDB!
// -------------------------------------------------------------------------------------
// We pass no main arguments...
CefRuntime.Initialize(new CefMainArgs(Array.Empty<string>()), settings, this, IntPtr.Zero);
// TODO CEF: After this point, debugging breaks. No, literally. My client crashes but ONLY with the debugger.
// I have tried using the DEBUG and RELEASE versions of libcef.so, stripped or non-stripped...
// And nothing seemed to work. Odd.
_initialized = true;
}
private void BrowseCommand(IConsoleShell shell, string argstr, string[] args)
{
if (!_initialized)
{
shell.WriteError("CEF is not initialized!");
return;
}
if (args.Length != 1)
{
shell.WriteError("Incorrect amount of arguments! Must be a single one.");
return;
}
var window = new SS14Window();
var browser = new BrowserControl();
if (args.Length < 1)
return;
browser.MouseFilter = Control.MouseFilterMode.Stop;
window.MouseFilter = Control.MouseFilterMode.Pass;
window.Contents.AddChild(browser);
browser.Browse(args[0]);
window.Open();
}
/// <summary>
/// Needs to be called regularly for CEF to keep working.
/// </summary>
public void Update()
{
DebugTools.Assert(_initialized);
// Calling this makes CEF do its work, without using its own update loop.
CefRuntime.DoMessageLoopWork();
}
/// <summary>
/// Call before program shutdown.
/// </summary>
public void Shutdown()
{
DebugTools.Assert(_initialized);
Dispose(true);
CefRuntime.Shutdown();
}
protected override CefBrowserProcessHandler GetBrowserProcessHandler()
{
return _browserProcessHandler;
}
protected override CefRenderProcessHandler GetRenderProcessHandler()
{
return _renderProcessHandler;
}
protected override void OnBeforeCommandLineProcessing(string processType, CefCommandLine commandLine)
{
// Disable zygote. TODO CEF: Do research on this?
commandLine.AppendSwitch("--no-zygote");
// We use single-process for now as multi-process requires us to ship a native program
commandLine.AppendSwitch("--single-process");
// We do CPU rendering, disable the GPU...
commandLine.AppendSwitch("--disable-gpu");
commandLine.AppendSwitch("--disable-gpu-compositing");
commandLine.AppendSwitch("--in-process-gpu");
Logger.Debug($"{commandLine}");
}
// TODO CEF: Research - Is this even needed?
private class BrowserProcessHandler : CefBrowserProcessHandler
{
}
// TODO CEF: Research - Is this even needed?
private class RenderProcessHandler : CefRenderProcessHandler
{
}
public void PostInject()
{
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using Xilium.CefGlue;
namespace Robust.Client.CEF
{
public static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)
{
CefRuntime.Load();
var mainArgs = new CefMainArgs(args);
var app = new SubprocessApp();
// This will block executing IF this is a proper subprocess but it was broken and it returned -1
// -1 means this process is the main method which... Wasn't possible.
// We probably need a native program?
var code = CefRuntime.ExecuteProcess(mainArgs, app, IntPtr.Zero);
if (code != 0)
{
System.Console.WriteLine($"CEF Subprocess exited with exit code {code}! Arguments: {string.Join(' ', args)}");
}
return code;
}
// Not really needed.
private class SubprocessApp : CefApp
{
protected override void OnBeforeCommandLineProcessing(string processType, CefCommandLine commandLine)
{
base.OnBeforeCommandLineProcessing(processType, commandLine);
// Just the same stuff as in CefManager.
commandLine.AppendSwitch("--no-zygote");
commandLine.AppendSwitch("--disable-gpu");
commandLine.AppendSwitch("--disable-gpu-compositing");
commandLine.AppendSwitch("--in-process-gpu");
System.Console.WriteLine($"SUBPROCESS COMMAND LINE: {commandLine.ToString()}");
}
}
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Properties.targets" />
<Import Project="..\MSBuild\Robust.Engine.props" />
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>WinExe</OutputType>
</PropertyGroup>
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<Target Name="RobustAfterBuild" AfterTargets="Build" />
<Import Project="..\MSBuild\Robust.Engine.targets" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\cefglue\CefGlue\CefGlue.csproj" />
<ProjectReference Include="..\Robust.Client\Robust.Client.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
using Xilium.CefGlue;
namespace Robust.Client.CEF
{
// Simple web client.
internal class RobustWebClient : CefClient
{
private readonly CefRenderHandler _renderHandler;
internal RobustWebClient(CefRenderHandler handler)
{
_renderHandler = handler;
}
protected override CefRenderHandler GetRenderHandler()
{
return _renderHandler;
}
}
}

1
cefglue Submodule

Submodule cefglue added at 7810f236c5