Avalonia: How to create a custom rendering control ?

Created on 14 Dec 2018  路  13Comments  路  Source: AvaloniaUI/Avalonia

Hi,

I'm currently working on a game engine for my company, and with a lot of research, we found that Avalonia is the best choice for our cross platform game editor. However, I'm always new to Avalonia so excuse me if my problem is not really a problem or if I'm wrong somewhere...

I've understand that Avalonia use Skia + Direct2D or OpenGL backend for rendering the whole window, so its not very difficult to create a custom renderer for a whole window rendering. But what I want is not to render onto the whole window but just somewhere in it (like the scene manager of Unity3D for example).

I've tried to create an offscreen game rendering thread to render the game, and exporting the framebuffer texture to an image stream. Into the MainWindow, we have an Image control, which at each render passes of the game background thread, changes its Source property with the exported image stream.

This process is working, but we have a - unsupportable - performance issue because the rendered image seems to skip too much frames. We have tried to solve this issue but we got nothing satisfying our needs...

So my question is to know if I'm doing things wrong ? Avalonia has already a solution for this case ? Is there a better way to implement this rendering control ?

Thanks in advance.

enhancement rendering

Most helpful comment

Another thing to consider is that DeferredRenderer's loop runs on 60FPS tick rate on a separate thread. We might want to add a way to hook to said renderer pass and update the bitmap. That would require to associate a layer to a control, but that should be doable. I'll try to figure something this month.

The API would look somewhat like:

class MyControl : Control, IThreadSafeRenderControl
{
  WritableBitmap _bitmap = new WritableBitmap(100, 100);
  bool IThreadSafeRenderControl.ThreadSafeRender(Func<DrawingContext> getContext, Size dimensions, bool alwaysProduceFrame)
  {
     using(var fb = bitmap.Lock())
     {
        // Do stuff
     }
     getContext().DrawBitmap(_bitmap);
     return true; // We have produced a new frame
  }
}

This method would be called on every frame. If there is something new, you'll need to draw the bitmap.

All 13 comments

Unfortunately, there is no easy solution.

WritableBitmap would be faster than saving and loading the image to stream, but that will still involve a transfer from video memory to the main memory and back.

For more performance we need a way to interact with native framebuffer texture like WPFs D3DImage does. The problem here is the variety of rendering APIs that can be actually used. Skia has support for OpenGL, OpenGL ES and Vulkan

Win32:

  • Direct2D
  • OpenGL ES 2.0 via ANGLE over DirectX 9
  • Desktop GL via ANGLE over DirectX 9
  • OpenGL ES 2.0 (3.0) via ANGLE over DirectX 11
  • Desktop GL via ANGLE over DirectX 11
  • Actual Desktop GL via vendor-provided driver (not implemented because it's unreliable)

Linux:

  • OpenGL ES over libEGL
  • Desktop GL over libEGL
  • Desktop GL over GLX (not implemented but planned)
  • Vulkan (not implemented, but Skia supports it)

Mac OS X:

  • Desktop GL via NSOpenGLContext
  • Vulkan over MoltenVK (not implemented, but Skia supports Vulkan and we need something that runs on Metal because of OpenGL deprecation)
    OpenGL ES via GLOVE + MoltenVK (not implemented, but we need some way to utilize OpenGL APIs for our 3D widget in the future).

We have a set of abstractions for OpenGL/OpenGLES that allows us to manipulate OpenGL contexts in somewhat unified way: GlInterface, IGlContext and IGlDisplay.

One can obtain glGetProcAddress from said abstraction, but that will most likely be not sufficient for a game engine that probably wants to initialize OpenGL itself. Another problem is that on some platforms we are using OpenGLES, on other desktop OpenGL gets initialized.

We could expose a way to provide your own SkImage as a bitmap, but it has to be created by the same GL context.

Another way would be to implement our windowing platform interfaces on top of your game engine and render Avalonia UI to a texture that gets later composed by the game engine itself.

Another a bit hacky way would be to replace our IWindowingPlatformGlFeature in service locator (you also would need to replace rendering platform initializer, since it's consumed by Skia backend on initialization) and create a decorator for IWindowImpl that provides a different set of surfaces. Then you could use HWND/XID to initialize OpenGL context manually and provide us with our OpenGL abstraction interfaces. That would cover Windows and Linux. On OS X you could propbably use our opengl context since it's created via NSOpenGLContext anyway. We might want to provide a way to customize the pixel format though.

I like the idea of having a special Bitmap for interop purposes. For Direct2D that would mean someone has to produce a shared bitmap from a scene graph he wants to display. That could very much be a 3D scene. Refreshing will still involve some invalidate visual calls. This solution isn't high performance but should still perform well. Rendering the Avalonia scene graph to some existing surface would perform better.

Thanks @kekekeks for your tips

WritableBitmap would be faster than saving and loading the image to stream, but that will still involve a transfer from video memory to the main memory and back.

Yes is true, with the use of WritableBitmap we have managed to run Avalonia from 3 - 7 FPS to 10 - 16 FPS (yes... It was up to 7 FPS before...).

Another way would be to implement our windowing platform interfaces on top of your game engine and render Avalonia UI to a texture that gets later composed by the game engine itself.

Another a bit hacky way would be to replace our IWindowingPlatformGlFeature in service locator (you also would need to replace rendering platform initializer, since it's consumed by Skia backend on initialization) and create a decorator for IWindowImpl that provides a different set of surfaces. Then you could use HWND/XID to initialize OpenGL context manually and provide us with our OpenGL abstraction interfaces.

Thats it's unfortunately not on what we want to focus in our team... Our game engine uses OpenGL, OpenGL ES, Vulkan, DirectX and Metal backends (not only OpenGL), depending on the executing platform and user settings. It will not easy for us to cover all these API for a proxy Avalonia renderer. And, if I understand well, we also have to create a proxy for Avalonia controls events, which means for us a lot of work that will not fit in our deadlines.

With a benchmark test, we can say that our game rendering loop (running with up to 26 FPS) is not synchronized to the Avalonia renderer loop (running with up to 16 FPS), this is caused by invalidating the Image control at every game rendering passes, but the control will be rerendered at each Avalonia rendering passes.

To bypass the Avalonia renderer (only for the Image control), after a - small - deep look at the Avalonia source code, we found a ImmediateRenderer class (Hallelujah!) which have a static Render(IVisual, IRenderTarget) method. In our window we have replaced the invalidate visual calls to ImmediateRenderer.Render(sceneViewer, _window.CreateRenderTarget()) and (Hallelujah!), the Avalonia renderer loop runs with up to 24 FPS, with a slight same average than the game rendering loop, but, again, we got a - incomprehensible - rendering issue, some times, the whole window rendering got freezed (nothing is refreshed), when not, the Image control is not rendered at the normal position and blink 4 - 6 times before disappearing totally and the window rendering got freezed again. This process restarts until we close the window.

There is a way to get the current IRenderTarget of the window ? (Sincerely, I don't think that Window.CreateRenderTarget() should be used here...) Or if it's possible, there is another way to bypass the Avalonia renderer for a control ?

If it may help, we are running tests in a PC with our minimal requirements target:

  • Linux:

    • OS: Kubuntu 18.04.1

    • RAM: 6GB

    • CPU: Intel Core i3 2.3Ghz x 4

    • Graphics card chipset: Intel Ironlake (yes... It's old but we want to support at least Ironlake chipsets and equivalent)

    • OpenGL version: 2.1

    • OpenGL ES version: 2.0

    • GLX version: 1.4

For now we are only testing on Linux platforms (due to the broken state of the DirectX backend on Windows).

Thanks again for your help.

If you are planning to use ImmediateRenderer, you need to switch to it when initializing windowing platform (UseWin32, UseGtk3, UseAvaloniaNative) calls, otherwise rendering would be mixed with DeferredRenderer, which isn't a good thing.

Another thing to consider is that DeferredRenderer's loop runs on 60FPS tick rate on a separate thread. We might want to add a way to hook to said renderer pass and update the bitmap. That would require to associate a layer to a control, but that should be doable. I'll try to figure something this month.

The API would look somewhat like:

class MyControl : Control, IThreadSafeRenderControl
{
  WritableBitmap _bitmap = new WritableBitmap(100, 100);
  bool IThreadSafeRenderControl.ThreadSafeRender(Func<DrawingContext> getContext, Size dimensions, bool alwaysProduceFrame)
  {
     using(var fb = bitmap.Lock())
     {
        // Do stuff
     }
     getContext().DrawBitmap(_bitmap);
     return true; // We have produced a new frame
  }
}

This method would be called on every frame. If there is something new, you'll need to draw the bitmap.

The API will be most likely changed a bit since we need something like that for playing GIF animations, but the general idea would be the same.

Casting your window to https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Visuals/Rendering/IRenderRoot.cs should give you the instance of the currently used renderer. Starting your App with UseDeferredRendering to false should produce a ImmediateRenderer. That instance has an overload of the Render method that accepts an IVisual and a DrawingContext. As far as I understand you can create a DrawingContext for the currently used RenderTarget. Maybe this helps a bit. Haven't checked this info.

@na2axl
Please, check if this is sufficient for your needs:
https://github.com/AvaloniaUI/Avalonia/pull/2185

You can test it by installing 0.7.1-build1014-beta build from our PR nuget feed

Thanks for your work @kekekeks I'll will try this tonight !

Build 0.7.1-build1021-beta should fix the issue with (0, 0) being passed as logicalSize.

:tada: :tada: :tada: Thanks very much @kekekeks for your PR, It's just awesome the window and the game loop are running at 60 FPS together ! No more graphics glitches and frame skips ! :tada: :tada: :tada:

Thanks again @kekekeks and @Gillibald for your help :smiley:

This PR comes with new exceptions thrown and debug warnings, but these doesn't block the program execution. If it may help you I will left this debug log here:

You may only use the Microsoft .NET Core Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software
to help you develop and test your applications.
-------------------------------------------------------------------
[DEBUG] Service added: AlienEngine.Core.IoC.IoCService
[DEBUG] Service added: AlienEngine.Core.Scripting.ScriptingService
[DEBUG] Service added: AlienEngine.Core.Messaging.MessagingService
[DEBUG] Detecting platform...
[DEBUG] Detected platform: X11
[DEBUG] Starting AlienEngine Editor...
Property: Property '"Avalonia.Media.TranslateTransform"."X"' is not registered on '"Avalonia.Controls.Border"'.
Property: Property '"Avalonia.Media.TranslateTransform"."X"' is not registered on '"Avalonia.Controls.Border"'.
Property: Property '"Avalonia.Media.TranslateTransform"."Y"' is not registered on '"Avalonia.Controls.Border"'.
Property: Property '"Avalonia.Media.TranslateTransform"."Y"' is not registered on '"Avalonia.Controls.Border"'.
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
The thread 12494 has exited with code 0 (0x0).
The thread 12482 has exited with code 0 (0x0).
The thread 12486 has exited with code 0 (0x0).
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
Not replacing existing, living, managed instance with new object.
Visual: Exception in render loop: "System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Rendering.DeferredRenderer.Render(Boolean forceComposite) in D:\a\1\s\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 240
   at Avalonia.Rendering.RenderLoop.TimerTick(TimeSpan time) in D:\a\1\s\src\Avalonia.Visuals\Rendering\RenderLoop.cs:line 120"
[DEBUG] Shutting down AlienEngine Editor...

(dotnet:12438): GLib-GObject-CRITICAL **: 23:42:55.252: g_object_unref: assertion 'G_IS_OBJECT (object)' failed
The program '[12438] Editor.dll' has exited with code 0 (0x0).

Lines starting with [DEBUG] are from our game engine.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

grokys picture grokys  路  4Comments

JonathaN7Shepard picture JonathaN7Shepard  路  4Comments

SeleDreams picture SeleDreams  路  4Comments

RUSshy picture RUSshy  路  4Comments

x2bool picture x2bool  路  4Comments