Multi-platform .NET bindings to the Ultralight project.
1using Silk.NET.OpenGLES;
2using System;
3using System.Diagnostics;
4using System.Drawing;
5using System.IO;
6using System.Runtime.CompilerServices;
7using System.Runtime.ExceptionServices;
8using System.Runtime.InteropServices;
9using System.Text;
10using CommandLine;
11using ImpromptuNinjas.UltralightSharp.Safe;
12using InlineIL;
13using Nvidia.Nsight.Injection;
14using Silk.NET.Core;
15using Silk.NET.EGL;
16using Silk.NET.GLFW;
17using Silk.NET.Windowing.Common;
18using Ultz.SuperInvoke;
19using Ultz.SuperInvoke.Loader;
20using ErrorCode = Silk.NET.GLFW.ErrorCode;
21using Renderer = ImpromptuNinjas.UltralightSharp.Safe.Renderer;
22using Window = Silk.NET.Windowing.Window;
23
24partial class Program {
25
26 static Program() {
27 // ReSharper disable once InvertIf
28 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
29 Windows.User32.SetProcessDPIAware();
30 Windows.User32.SetProcessDpiAwarenessContext(Windows.User32.DpiAwarenessContext.PerMonitorAwareV2);
31 Windows.ShCore.SetProcessDpiAwareness(Windows.ShCore.ProcessDpiAwareness.PerMonitorDpiAware);
32 }
33 }
34
35 private static int _majOES = 3;
36
37 private static readonly string AsmPath = new Uri(typeof(Program).Assembly.CodeBase!).LocalPath;
38
39 private static readonly string AsmDir = Path.GetDirectoryName(AsmPath)!;
40
41 private static readonly string AssetsDir = Path.Combine(AsmDir, "assets");
42
43 private static IView _snView = null!;
44
45 private static GL _gl = null!;
46
47 private static uint _qvb;
48
49 private static uint _qeb;
50
51 private static uint _qva;
52
53 private static uint _qpg;
54
55 private static Renderer _ulRenderer = null!;
56
57 private static Session _ulSession = null!;
58
59 private static View _ulView = null!;
60
61 private static Glfw _glfw = null!;
62
63 private static string _storagePath = null!;
64
65 private static bool _useOpenGl;
66
67 private static bool _automaticFallback = true;
68
69 private static bool _useEgl = true;
70
71 private static int _glMaj = 3;
72
73 private static int _glMin = 2;
74
75 private static unsafe void Main(string[] args) {
76 var parsedOpts = Parser.Default.ParseArguments<Options>(args);
77
78 parsedOpts.WithParsed(opts => {
79 if (opts.Context != null) {
80 _useEgl = opts.Context.Contains("egl");
81 }
82
83 if (opts.GlApi != null) {
84 _automaticFallback = false;
85 _useOpenGl = !opts.GlApi.Contains("es");
86 }
87
88 if (opts.GlMajorVersion != null) {
89 _automaticFallback = false;
90 _glMaj = opts.GlMajorVersion.Value;
91 }
92
93 if (opts.GlMinorVersion != null) {
94 _automaticFallback = false;
95 _glMin = opts.GlMinorVersion.Value;
96 }
97 });
98
99 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
100 Console.OutputEncoding = Encoding.UTF8;
101 Ansi.WindowsConsole.TryEnableVirtualTerminalProcessing();
102 }
103
104 AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
105 AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException;
106
107 //InjectNsight();
108
109 //InjectRenderDoc();
110
111 var options = WindowOptions.Default;
112
113 var size = new Size(1024, 576);
114
115 var title = "UltralightSharp - Silk.NET";
116
117 options.Size = size;
118 options.Title = title;
119 options.VSync = VSyncMode.On;
120 options.TransparentFramebuffer = false;
121 options.PreferredDepthBufferBits = null;
122 //options.VSync = true;
123
124 /*
125 if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
126 var asmDir = Path.GetDirectoryName(new Uri(typeof(Program).Assembly.CodeBase!).LocalPath)!;
127 var glfwPath = Path.Combine(asmDir, "libglfw.so.3");
128 const string sysGlfwPath = "/lib/libglfw.so.3";
129 if (File.Exists(sysGlfwPath)) {
130 var sb = new StringBuilder(1024);
131 var sbSize = (UIntPtr)sb.Capacity;
132 var used = (long)Libc.readlink(glfwPath, sb, sbSize);
133 if (used >= 0) {
134 var link = sb.ToString(0, (int)(used - 1));
135 if (link != sysGlfwPath) {
136 File.Delete(glfwPath);
137 Libc.symlink(sysGlfwPath, glfwPath);
138 }
139 }
140 else {
141 // not a link
142 File.Delete(glfwPath);
143 Libc.symlink(sysGlfwPath, glfwPath);
144 Cleanup += () => {
145 File.Delete(glfwPath);
146 };
147 }
148 }
149 }
150 */
151
152 _glfw = Glfw.GetApi();
153 Console.WriteLine($"GLFW v{_glfw.GetVersionString()}");
154
155 if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
156 _glfw.InitHint(InitHint.CocoaMenubar, false);
157 _glfw.InitHint(InitHint.CocoaChdirResources, false);
158 }
159
160 _glfw = GlfwProvider.GLFW.Value;
161
162 {
163 // setup logging
164 Ultralight.SetLogger(new Logger {LogMessage = LoggerCallback});
165
166 var tempDir = Path.GetTempPath();
167 // find a place to stash instance storage
168 do _storagePath = Path.Combine(tempDir, Guid.NewGuid().ToString());
169 while (Directory.Exists(_storagePath) || File.Exists(_storagePath));
170
171 AppCore.EnablePlatformFontLoader();
172
173 AppCore.EnablePlatformFileSystem(AssetsDir);
174 }
175
176 /* afaik GLFW already does this, this was just to check
177 if (_useEgl || _automaticFallback) {
178 Console.WriteLine("Binding to LibEGL...");
179 var eglLib = new UnmanagedLibrary(
180 new CustomEglLibNameContainer().GetLibraryName(),
181 LibraryLoader.GetPlatformDefaultLoader()
182 );
183 var q = eglLib.LoadFunction("eglQueryAPI");
184 IL.Push(q);
185 IL.Emit.Calli(StandAloneMethodSig.UnmanagedMethod(CallingConvention.Cdecl, typeof(EGLEnum)));
186 IL.Pop(out EGLEnum api);
187 Console.WriteLine($"EGL API Target: {api}");
188 var b = eglLib.LoadFunction("eglBindAPI");
189 if (_useOpenGL && api != EGLEnum.OpenglApi) {
190 IL.Push(EGLEnum.OpenglApi);
191 IL.Push(b);
192 IL.Emit.Calli(StandAloneMethodSig.UnmanagedMethod(CallingConvention.Cdecl, typeof(bool),
193 typeof(EGLEnum)));
194 IL.Pop(out bool success);
195 Console.Error.WriteLine(!success
196 ? "Couldn't bind EGL to OpenGL"
197 : "EGL now bound to OpenGL");
198 }
199 else if (!_useOpenGL && api != EGLEnum.OpenglESApi){
200 IL.Push(EGLEnum.OpenglESApi);
201 IL.Push(b);
202 IL.Emit.Calli(StandAloneMethodSig.UnmanagedMethod(CallingConvention.Cdecl, typeof(bool),
203 typeof(EGLEnum)));
204 IL.Pop(out bool success);
205 Console.Error.WriteLine(!success
206 ? "Couldn't bind EGL to OpenGL ES"
207 : "EGL now bound to OpenGL ES");
208 }
209 }
210 */
211
212 if (_automaticFallback) {
213 Console.WriteLine("Checking for supported context...");
214
215 for (;;) {
216 SetGlfwWindowHints();
217
218 Console.WriteLine(
219 _useOpenGl
220 ? "Attempting OpenGL v3.2 (Core)"
221 : $"Attempting OpenGL ES v{_majOES}.0");
222 var wh = _glfw.CreateWindow(1024, 576, title, null, null);
223 if (wh != null) {
224 Console.WriteLine(
225 _useOpenGl
226 ? "Created Window with OpenGL v3.2 (Core)"
227 : $"Created Window with OpenGL ES v{_majOES}.0");
228 _glfw.DestroyWindow(wh);
229 break;
230 }
231
232 var code = _glfw.GetError(out char* pDesc);
233 if (code == ErrorCode.NoError || pDesc == null)
234 throw new PlatformNotSupportedException("Can't create a window via GLFW. Unknown error.");
235
236 var strLen = new ReadOnlySpan<byte>((byte*) pDesc, 32768).IndexOf<byte>(0);
237 if (strLen == -1) strLen = 0;
238 var str = new string((sbyte*) pDesc, 0, strLen, Encoding.UTF8);
239 var errMsg = $"{code}: {str}";
240 Console.Error.WriteLine(errMsg);
241 if (code != ErrorCode.VersionUnavailable)
242 throw new GlfwException(errMsg);
243
244 // attempt sequence: OpenGL ES 3.0, OpenGL 3.2, OpenGL ES 2.0
245 if (!_useOpenGl && _majOES == 3)
246 _useOpenGl = true;
247 else if (_majOES == 3 && _useOpenGl) {
248 _useOpenGl = false;
249 _majOES = 2;
250 }
251 else
252 throw new GlfwException(errMsg);
253 }
254 }
255
256 SetGlfwWindowHints();
257
258 if (_useOpenGl)
259 options.API = new GraphicsAPI(
260 ContextAPI.OpenGL,
261 ContextProfile.Core,
262 ContextFlags.ForwardCompatible,
263 new APIVersion(_automaticFallback ? 3 : _glMaj, _automaticFallback ? 2 : _glMin)
264 );
265 else
266 options.API = new GraphicsAPI(
267 ContextAPI.OpenGLES,
268 ContextProfile.Core,
269 ContextFlags.ForwardCompatible,
270 new APIVersion(_automaticFallback ? _majOES : _glMaj, _automaticFallback ? 0 : _glMin)
271 );
272 options.IsVisible = true;
273 options.WindowBorder = WindowBorder.Resizable;
274 options.WindowState = WindowState.Normal;
275
276 Console.WriteLine("Creating window...");
277
278 _snView = Window.Create(options);
279
280 _snView.Load += OnLoad;
281 _snView.Render += OnRender;
282 _snView.Update += OnUpdate;
283 _snView.Closing += OnClose;
284 _snView.Resize += OnResize;
285
286 var glCtx = _snView.GLContext;
287
288 if (!_useOpenGl) {
289 Console.WriteLine("Binding to LibGLES...");
290 _gl = LibraryActivator.CreateInstance<GL>
291 (
292 new CustomGlEsLibNameContainer().GetLibraryName(),
293 TemporarySuperInvokeClass.GetLoader(glCtx)
294 );
295 }
296
297 Console.WriteLine("Initializing window...");
298
299 _snView.Initialize();
300
301 if (_snView.Handle == null) {
302 var code = _glfw.GetError(out char* pDesc);
303 if (code == ErrorCode.NoError || pDesc == null)
304 throw new PlatformNotSupportedException("Can't create a window via GLFW. Unknown error.");
305
306 var strLen = new ReadOnlySpan<byte>((byte*) pDesc, 32768).IndexOf<byte>(0);
307 if (strLen == -1) strLen = 0;
308 var str = new string((sbyte*) pDesc, 0, strLen, Encoding.UTF8);
309 throw new GlfwException($"{code}: {str}");
310 }
311
312 Console.WriteLine("Starting main loop...");
313 _snView.Run();
314 }
315
316 private static void SetGlfwWindowHints() {
317 _glfw.DefaultWindowHints();
318 if (_useOpenGl) {
319 _glfw.WindowHint(WindowHintContextApi.ContextCreationApi,
320 _automaticFallback || !_useEgl ? ContextApi.NativeContextApi : ContextApi.EglContextApi);
321 _glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGL);
322 _glfw.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core);
323
324 _glfw.WindowHint(WindowHintInt.ContextVersionMajor, 3);
325 _glfw.WindowHint(WindowHintInt.ContextVersionMinor, 2);
326 }
327 else {
328 _glfw.WindowHint(WindowHintContextApi.ContextCreationApi,
329 _automaticFallback || _useEgl ? ContextApi.EglContextApi : ContextApi.NativeContextApi);
330 _glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGLES);
331
332 _glfw.WindowHint(WindowHintInt.ContextVersionMajor, _majOES);
333 _glfw.WindowHint(WindowHintInt.ContextVersionMinor, 0);
334 }
335
336 _glfw.WindowHint(WindowHintInt.RedBits, 8);
337 _glfw.WindowHint(WindowHintInt.GreenBits, 8);
338 _glfw.WindowHint(WindowHintInt.BlueBits, 8);
339 _glfw.WindowHint(WindowHintInt.DepthBits, 24);
340 _glfw.WindowHint(WindowHintInt.StencilBits, 8);
341
342 if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
343 // osx graphics switching
344 _glfw.WindowHint((WindowHintBool) 0x00023003, true);
345 }
346 }
347
348 public static event Action? Cleanup;
349
350 private static unsafe void OnFirstChanceException(object? sender, FirstChanceExceptionEventArgs eventArgs) {
351 var sf = new StackFrame(1, true);
352 Console.Error.WriteLine("First-Chance Exception Stack Frame:");
353 Console.Error.WriteLine(sf.ToString());
354 Console.Error.WriteLine($"First-Chance Exception: {eventArgs.Exception.GetType().Name}: {eventArgs.Exception.Message}");
355 }
356
357 private static unsafe void OnUnhandledException(object sender, UnhandledExceptionEventArgs eventArgs) {
358 var st = new StackTrace(1, true);
359 Console.Error.WriteLine("Unhandled Exception Entry Stack:");
360 Console.Error.WriteLine(st.ToString());
361 st = new StackTrace((Exception) eventArgs.ExceptionObject, 0, true);
362 Console.Error.WriteLine("Unhandled Exception:");
363 Console.Error.WriteLine(st.ToString());
364 if (eventArgs.IsTerminating) {
365 Cleanup?.Invoke();
366 }
367 }
368
369 private static unsafe void OnResize(Size size) {
370 _ulView.Resize((uint) size.Width, (uint) size.Height);
371 }
372
373 private static void InjectRenderDoc() {
374 try {
375 Console.WriteLine("Injecting RenderDoc...");
376 using var proc = Process.GetCurrentProcess();
377 var ps = Process.Start(@"\\?\C:\Program Files\RenderDoc\renderdoccmd.exe", $"inject --PID={proc.Id} --opt-disallow-vsync --opt-disallow-fullscreen --opt-ref-all-resources");
378 ps?.WaitForExit(250);
379 //Process.Start(@"\\?\C:\Program Files\RenderDoc\qrenderdoc.exe", $"inject --PID={proc.Id}");
380 }
381 catch (Exception ex) {
382 Console.Error.WriteLine("Couldn't inject RenderDoc");
383 Console.Error.WriteLine(ex.ToString());
384 Console.Error.Flush();
385 }
386 }
387
388 private static unsafe void InjectNsight() {
389 try {
390 Console.WriteLine("Injecting Nsight...");
391 NativeLibrary.Load(@"C:\Program Files\NVIDIA Corporation\Nsight Graphics 2020.3.1\SDKs\NsightGraphicsSDK\0.7.0\lib\x64\NGFX_Injection.dll");
392 var installCount = 0;
393 var r = Nsight.EnumerateInstallations((uint*) &installCount, null);
394 if (r != NsightInjectionResult.Ok) throw new Exception(r.ToString());
395
396 var installs = stackalloc NsightInjectionInstallationInfo[installCount];
397 r = Nsight.EnumerateInstallations((uint*) &installCount, installs);
398 if (r != NsightInjectionResult.Ok) throw new Exception(r.ToString());
399
400 var injected = false;
401 for (var x = 0; x < installCount; ++x) {
402 var activityCount = 0;
403 var install = &installs[x];
404 r = Nsight.EnumerateActivities(install, (uint*) &activityCount, null);
405 if (r != NsightInjectionResult.Ok) throw new Exception(r.ToString());
406
407 // ReSharper disable once StackAllocInsideLoop
408 var activities = stackalloc NsightInjectionActivity[activityCount];
409 r = Nsight.EnumerateActivities(install, (uint*) &activityCount, activities);
410 if (r != NsightInjectionResult.Ok) throw new Exception(r.ToString());
411
412 for (var i = 0; i < activityCount; ++i) {
413 var activity = &activities[i];
414 if (activity->Type != NsightInjectionActivityType.FrameDebugger)
415 continue;
416
417 r = Nsight.InjectToProcess(install, activity);
418 if (r != NsightInjectionResult.Ok) throw new Exception(r.ToString());
419
420 injected = true;
421 break;
422 }
423
424 if (injected)
425 break;
426 }
427
428 if (!injected)
429 throw new Exception("Found no injectable FrameDebugger activity.");
430 }
431 catch (Exception ex) {
432 Console.Error.WriteLine("Couldn't inject Nsight");
433 Console.Error.WriteLine(ex.ToString());
434 Console.Error.Flush();
435 }
436 }
437
438 private static void LabelObject(ObjectIdentifier objId, uint vao, string name) {
439 if (_dbg == null)
440 return;
441
442 _dbg.ObjectLabel(objId, vao, (uint) name.Length, name);
443 CheckGl();
444 }
445
446 private static void OnUpdate(double obj) {
447 _ulRenderer.Update();
448 }
449
450 private static void OnClose() {
451 //Remember to delete the buffers.
452 _ulView.Dispose();
453 _ulRenderer.Dispose();
454 _ulSession.Dispose();
455 _gl.DeleteBuffer(_qvb);
456 _gl.DeleteBuffer(_qeb);
457 _gl.DeleteVertexArray(_qva);
458 _gl.DeleteProgram(_qpg);
459 }
460
461 private static void CheckGl([CallerLineNumber] int lineNumber = default) {
462#if DEBUG
463 var error = _gl.GetError();
464 if (error == default)
465 return;
466
467 Console.Error.WriteLine($"Line {lineNumber}, GL Error: {error}");
468 Console.Error.Flush();
469 //Debugger.Break();
470#endif
471 }
472
473}