Application.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. //
  2. // Support for bubbling up to C# the virtual methods calls for Setup, Start and Stop in Application
  3. //
  4. // This is done by using an ApplicationProxy in C++ that bubbles up
  5. //
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Diagnostics;
  9. using System.Linq;
  10. using System.Reflection;
  11. using System.Runtime.CompilerServices;
  12. using System.Runtime.InteropServices;
  13. using System.Threading.Tasks;
  14. using Urho.IO;
  15. using Urho.Audio;
  16. using Urho.Resources;
  17. using Urho.Actions;
  18. using Urho.Gui;
  19. using System.Threading;
  20. namespace Urho
  21. {
  22. [PreserveAttribute(AllMembers = true)]
  23. public partial class Application
  24. {
  25. // references needed to prevent GC from collecting callbacks passed to native code
  26. static ActionIntPtr setupCallback;
  27. static ActionIntPtr startCallback;
  28. static ActionIntPtr stopCallback;
  29. static TaskCompletionSource<bool> exitTask;
  30. static TaskCompletionSource<bool> waitFrameEndTaskSource;
  31. static int renderThreadId = -1;
  32. static bool isExiting = false;
  33. static List<Action> staticActionsToDispatch;
  34. static List<DelayState> delayTasks;
  35. List<Action> actionsToDispatch = new List<Action>();
  36. [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
  37. public delegate void ActionIntPtr(IntPtr value);
  38. [DllImport(Consts.NativeImport, CallingConvention = CallingConvention.Cdecl)]
  39. static extern IntPtr ApplicationProxy_ApplicationProxy(IntPtr contextHandle, ActionIntPtr setup, ActionIntPtr start, ActionIntPtr stop, string args, IntPtr externalWindow);
  40. static Application current;
  41. public static Application Current
  42. {
  43. get
  44. {
  45. if (current == null)
  46. throw new InvalidOperationException("The application is not configured yet");
  47. return current;
  48. }
  49. private set { current = value; }
  50. }
  51. public static bool HasCurrent => current != null;
  52. static Context currentContext;
  53. public static Context CurrentContext
  54. {
  55. get
  56. {
  57. if (currentContext == null)
  58. throw new InvalidOperationException("Urho.Application is not started yet. All urho objects should be initialized after app.Run() since they need an active Context.\n");
  59. return currentContext;
  60. }
  61. private set { currentContext = value; }
  62. }
  63. // see Drawable2D.h:66
  64. public const float PixelSize = 0.01f;
  65. [Preserve]
  66. public Application(ApplicationOptions options) : this(new Context(), options) { }
  67. Application(Context context, ApplicationOptions options = null) : base(UrhoObjectFlag.Empty)
  68. {
  69. //Workbooks specific:
  70. CancelActiveActionsOnStop = this is SimpleApplication;
  71. if (context == null)
  72. throw new ArgumentNullException(nameof(context));
  73. //keep references to callbacks (supposed to be passed to native code) as long as the App is alive
  74. setupCallback = ProxySetup;
  75. startCallback = ProxyStart;
  76. stopCallback = ProxyStop;
  77. #if !ANDROID
  78. if (context.Refs() < 1)
  79. context.AddRef();
  80. #endif
  81. Options = options ?? new ApplicationOptions(assetsFolder: null);
  82. handle = ApplicationProxy_ApplicationProxy(context.Handle, setupCallback, startCallback, stopCallback, Options.ToString(), Options.ExternalWindow);
  83. Runtime.RegisterObject(this);
  84. }
  85. public bool IsClosed { get; private set; }
  86. internal static ApplicationOptions CurrentOptions => current?.Options;
  87. internal object UrhoSurface { get; set; }
  88. /// <summary>
  89. /// Application options
  90. /// </summary>
  91. public ApplicationOptions Options { get; private set; }
  92. /// <summary>
  93. /// True means Urho3D is somewhere between E_BEGINFRAME and E_ENDFRAME in Engine::RunFrame()
  94. /// </summary>
  95. public bool IsFrameRendering { get; private set; }
  96. /// <summary>
  97. /// Frame update event
  98. /// </summary>
  99. public event Action<UpdateEventArgs> Update;
  100. /// <summary>
  101. /// Invoke actions in the Main Thread (the next Update call)
  102. /// </summary>
  103. public static void InvokeOnMain(Action action)
  104. {
  105. if (!HasCurrent)
  106. {
  107. if (staticActionsToDispatch == null)
  108. staticActionsToDispatch = new List<Action>();
  109. lock (staticActionsToDispatch)
  110. staticActionsToDispatch.Add(action);
  111. return;
  112. }
  113. var actions = Current.actionsToDispatch;
  114. lock (actions)
  115. {
  116. actions.Add(action);
  117. }
  118. }
  119. /// <summary>
  120. /// Dispatch to OnUpdate
  121. /// </summary>
  122. public static ConfiguredTaskAwaitable<bool> ToMainThreadAsync()
  123. {
  124. var tcs = new TaskCompletionSource<bool>();
  125. InvokeOnMain(() => tcs.TrySetResult(true));
  126. return tcs.Task.ConfigureAwait(false);
  127. }
  128. /// <summary>
  129. /// Invoke actions in the Main Thread (the next Update call)
  130. /// </summary>
  131. public static Task<bool> InvokeOnMainAsync(Action action)
  132. {
  133. var tcs = new TaskCompletionSource<bool>();
  134. InvokeOnMain(() =>
  135. {
  136. action?.Invoke();
  137. tcs.TrySetResult(true);
  138. });
  139. return tcs.Task;
  140. }
  141. public Task Delay(float seconds)
  142. {
  143. var tcs = new TaskCompletionSource<bool>();
  144. if (delayTasks == null)
  145. delayTasks = new List<DelayState>();
  146. delayTasks.Add(new DelayState { Duration = seconds, Task = tcs });
  147. return tcs.Task;
  148. }
  149. public Task Delay(TimeSpan timeSpan) => Delay((float)timeSpan.TotalSeconds);
  150. static Application GetApp(IntPtr h) => Runtime.LookupObject<Application>(h);
  151. void Time_FrameStarted(FrameStartedEventArgs args)
  152. {
  153. IsFrameRendering = true;
  154. }
  155. void Time_FrameEnded(FrameEndedEventArgs args)
  156. {
  157. IsFrameRendering = false;
  158. if (waitFrameEndTaskSource != null)
  159. waitFrameEndTaskSource?.TrySetResult(true);
  160. }
  161. void HandleUpdate(UpdateEventArgs args)
  162. {
  163. var timeStep = args.TimeStep;
  164. Update?.Invoke(args);
  165. ActionManager.Update(timeStep);
  166. OnUpdate(timeStep);
  167. if (staticActionsToDispatch != null)
  168. {
  169. lock (staticActionsToDispatch)
  170. foreach (var action in staticActionsToDispatch)
  171. action();
  172. staticActionsToDispatch = null;
  173. }
  174. if (actionsToDispatch.Count > 0)
  175. {
  176. lock (actionsToDispatch)
  177. {
  178. foreach (var action in actionsToDispatch)
  179. action();
  180. actionsToDispatch.Clear();
  181. }
  182. }
  183. if (delayTasks != null)
  184. {
  185. for (int i = 0; i < delayTasks.Count; i++)
  186. {
  187. var task = delayTasks[i];
  188. task.Duration -= timeStep;
  189. if (task.Duration <= 0)
  190. {
  191. task.Task.TrySetResult(true);
  192. delayTasks.RemoveAt(i);
  193. }
  194. }
  195. }
  196. }
  197. [MonoPInvokeCallback(typeof(ActionIntPtr))]
  198. static void ProxySetup(IntPtr h)
  199. {
  200. isExiting = false;
  201. Runtime.Setup();
  202. Current = GetApp(h);
  203. CurrentContext = Current.Context;
  204. Current.Setup();
  205. }
  206. [MonoPInvokeCallback(typeof(ActionIntPtr))]
  207. static void ProxyStart(IntPtr h)
  208. {
  209. Runtime.Start();
  210. Current = GetApp(h);
  211. Current.SubscribeToAppEvents();
  212. #if WINDOWS_UWP
  213. // UWP temp workaround:
  214. var text = new Text();
  215. text.SetFont(CoreAssets.Fonts.AnonymousPro, 1);
  216. text.Value = " ";
  217. Current.UI.Root.AddChild(text);
  218. #endif
  219. Current.Start();
  220. Started?.Invoke();
  221. #if ANDROID
  222. renderThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
  223. #endif
  224. }
  225. public static bool CancelActiveActionsOnStop { get; set; }
  226. [MonoPInvokeCallback(typeof(ActionIntPtr))]
  227. static void ProxyStop(IntPtr h)
  228. {
  229. isExiting = true;
  230. if (CancelActiveActionsOnStop)
  231. Current.ActionManager.CancelActiveActions();
  232. LogSharp.Debug("ProxyStop");
  233. UrhoPlatformInitializer.Initialized = false;
  234. var context = Current.Context;
  235. var app = GetApp(h);
  236. app.IsClosed = true;
  237. app.Stop();
  238. LogSharp.Debug("ProxyStop: Runtime.Cleanup");
  239. Runtime.Cleanup();
  240. LogSharp.Debug("ProxyStop: Disposing context");
  241. context.Dispose();
  242. Current = null;
  243. Stopped?.Invoke();
  244. LogSharp.Debug("ProxyStop: end");
  245. exitTask?.TrySetResult(true);
  246. }
  247. void SubscribeToAppEvents()
  248. {
  249. Engine.SubscribeToUpdate(HandleUpdate);
  250. Time.FrameStarted += Time_FrameStarted;
  251. Time.FrameEnded += Time_FrameEnded;
  252. }
  253. SemaphoreSlim stopSemaphore = new SemaphoreSlim(1);
  254. #if ANDROID
  255. static readonly object stopLock = new object();
  256. internal static void StopAndroid()
  257. {
  258. lock (stopLock)
  259. {
  260. if (current == null || !current.IsActive)
  261. return;
  262. LogSharp.Debug($"StopAndroid.");
  263. Current.Input.Enabled = false;
  264. isExiting = true;
  265. Current.Engine.Exit();
  266. Org.Libsdl.App.SDLSurface.JoinSDLThread();
  267. }
  268. GC.Collect();
  269. GC.WaitForPendingFinalizers();
  270. GC.Collect();
  271. }
  272. #endif
  273. internal static void WaitStart()
  274. {
  275. waitFrameEndTaskSource = new TaskCompletionSource<bool>();
  276. waitFrameEndTaskSource.Task.Wait(3000);
  277. }
  278. internal static async Task StopCurrent()
  279. {
  280. #if ANDROID
  281. StopAndroid();
  282. #endif
  283. if (current == null || !current.IsActive)
  284. return;
  285. Current.Input.Enabled = false;
  286. isExiting = true;
  287. #if IOS
  288. iOS.UrhoSurface.StopRendering(current);
  289. #endif
  290. #if WINDOWS_UWP && !UWP_HOLO
  291. UWP.UrhoSurface.StopRendering().Wait();
  292. #endif
  293. LogSharp.Debug($"StopCurrent: Current.IsFrameRendering={Current.IsFrameRendering}");
  294. if (Current.IsFrameRendering)// && !Current.Engine.PauseMinimized)
  295. {
  296. waitFrameEndTaskSource = new TaskCompletionSource<bool>();
  297. await waitFrameEndTaskSource.Task;
  298. LogSharp.Debug($"StopCurrent: waitFrameEndTaskSource awaited");
  299. waitFrameEndTaskSource = null;
  300. }
  301. LogSharp.Debug($"StopCurrent: Engine.Exit");
  302. Current.Engine.Exit();
  303. #if DESKTOP
  304. if (Current.Options.DelayedStart)
  305. #endif
  306. ProxyStop(Current.Handle);
  307. GC.Collect();
  308. GC.WaitForPendingFinalizers();
  309. GC.Collect();
  310. }
  311. public bool IsExiting => isExiting || Runtime.IsClosing || Engine.Exiting;
  312. public bool IsActive => !IsClosed && !IsDeleted && !Engine.IsDeleted && !IsExiting;
  313. public async Task Exit()
  314. {
  315. try
  316. {
  317. await stopSemaphore.WaitAsync();
  318. if (!IsActive)
  319. return;
  320. await StopCurrent();
  321. }
  322. finally
  323. {
  324. stopSemaphore.Release();
  325. }
  326. }
  327. protected override bool AllowNativeDelete => false;
  328. protected virtual void Setup() { }
  329. public static event Action Started;
  330. protected virtual void Start() { }
  331. public static event Action Stopped;
  332. protected virtual void Stop() { }
  333. protected virtual void OnUpdate(float timeStep) { }
  334. internal ActionManager ActionManager { get; } = new ActionManager();
  335. [DllImport(Consts.NativeImport, EntryPoint = "Urho_GetPlatform", CallingConvention = CallingConvention.Cdecl)]
  336. static extern IntPtr GetPlatform();
  337. static Platforms platform;
  338. public static Platforms Platform
  339. {
  340. get
  341. {
  342. Runtime.Validate(typeof(Application));
  343. if (platform == Platforms.Unknown)
  344. platform = PlatformsMap.FromString(Marshal.PtrToStringAnsi(GetPlatform()));
  345. return platform;
  346. }
  347. }
  348. //
  349. // GetSubsystem helpers
  350. //
  351. ResourceCache resourceCache;
  352. public ResourceCache ResourceCache
  353. {
  354. get
  355. {
  356. Runtime.Validate(typeof(Application));
  357. if (resourceCache == null)
  358. resourceCache = new ResourceCache(UrhoObject_GetSubsystem(handle, ResourceCache.TypeStatic.Code));
  359. return resourceCache;
  360. }
  361. }
  362. UrhoConsole console;
  363. public UrhoConsole Console
  364. {
  365. get
  366. {
  367. Runtime.Validate(typeof(Application));
  368. if (console == null)
  369. console = new UrhoConsole(UrhoObject_GetSubsystem(handle, UrhoConsole.TypeStatic.Code));
  370. return console;
  371. }
  372. }
  373. Urho.Network.Network network;
  374. public Urho.Network.Network Network
  375. {
  376. get
  377. {
  378. Runtime.Validate(typeof(Application));
  379. if (network == null)
  380. network = new Urho.Network.Network(UrhoObject_GetSubsystem(handle, Urho.Network.Network.TypeStatic.Code));
  381. return network;
  382. }
  383. }
  384. Time time;
  385. public Time Time
  386. {
  387. get
  388. {
  389. Runtime.Validate(typeof(Application));
  390. if (time == null)
  391. time = new Time(UrhoObject_GetSubsystem(handle, Time.TypeStatic.Code));
  392. return time;
  393. }
  394. }
  395. WorkQueue workQueue;
  396. public WorkQueue WorkQueue
  397. {
  398. get
  399. {
  400. Runtime.Validate(typeof(Application));
  401. if (workQueue == null)
  402. workQueue = new WorkQueue(UrhoObject_GetSubsystem(handle, WorkQueue.TypeStatic.Code));
  403. return workQueue;
  404. }
  405. }
  406. Profiler profiler;
  407. public Profiler Profiler
  408. {
  409. get
  410. {
  411. Runtime.Validate(typeof(Application));
  412. if (profiler == null)
  413. profiler = new Profiler(UrhoObject_GetSubsystem(handle, Profiler.TypeStatic.Code));
  414. return profiler;
  415. }
  416. }
  417. FileSystem fileSystem;
  418. public FileSystem FileSystem
  419. {
  420. get
  421. {
  422. Runtime.Validate(typeof(Application));
  423. if (fileSystem == null)
  424. fileSystem = new FileSystem(UrhoObject_GetSubsystem(handle, FileSystem.TypeStatic.Code));
  425. return fileSystem;
  426. }
  427. }
  428. Log log;
  429. public Log Log
  430. {
  431. get
  432. {
  433. Runtime.Validate(typeof(Application));
  434. if (log == null)
  435. log = new Log(UrhoObject_GetSubsystem(handle, Log.TypeStatic.Code));
  436. return log;
  437. }
  438. }
  439. Input input;
  440. public Input Input
  441. {
  442. get
  443. {
  444. Runtime.Validate(typeof(Application));
  445. if (input == null)
  446. input = new Input(UrhoObject_GetSubsystem(handle, Input.TypeStatic.Code));
  447. return input;
  448. }
  449. }
  450. Urho.Audio.Audio audio;
  451. public Urho.Audio.Audio Audio
  452. {
  453. get
  454. {
  455. Runtime.Validate(typeof(Application));
  456. if (audio == null)
  457. audio = new Audio.Audio(UrhoObject_GetSubsystem(handle, Urho.Audio.Audio.TypeStatic.Code));
  458. return audio;
  459. }
  460. }
  461. UI uI;
  462. public UI UI
  463. {
  464. get
  465. {
  466. Runtime.Validate(typeof(Application));
  467. if (uI == null)
  468. uI = new UI(UrhoObject_GetSubsystem(handle, UI.TypeStatic.Code));
  469. return uI;
  470. }
  471. }
  472. Graphics graphics;
  473. public Graphics Graphics
  474. {
  475. get
  476. {
  477. Runtime.Validate(typeof(Application));
  478. if (graphics == null)
  479. graphics = new Graphics(UrhoObject_GetSubsystem(handle, Graphics.TypeStatic.Code));
  480. return graphics;
  481. }
  482. }
  483. Renderer renderer;
  484. public Renderer Renderer
  485. {
  486. get
  487. {
  488. Runtime.Validate(typeof(Application));
  489. if (renderer == null)
  490. renderer = new Renderer(UrhoObject_GetSubsystem(handle, Renderer.TypeStatic.Code));
  491. return renderer;
  492. }
  493. }
  494. [DllImport(Consts.NativeImport, CallingConvention = CallingConvention.Cdecl)]
  495. extern static IntPtr Application_GetEngine(IntPtr handle);
  496. Engine engine;
  497. public Engine Engine
  498. {
  499. get
  500. {
  501. if (engine == null)
  502. engine = new Engine(Application_GetEngine(handle));
  503. return engine;
  504. }
  505. }
  506. public static T CreateInstance<T>(ApplicationOptions options = null) where T : Application
  507. {
  508. return (T)CreateInstance(typeof(T), options);
  509. }
  510. public static Application CreateInstance(Type applicationType, ApplicationOptions options = null)
  511. {
  512. try
  513. {
  514. return (Application)Activator.CreateInstance(applicationType, options);
  515. }
  516. catch (Exception exc)
  517. {
  518. throw new InvalidOperationException($"Constructor {applicationType}(ApplicationOptions) was not found.", exc);
  519. }
  520. }
  521. internal static void ThrowUnhandledException(Exception exc)
  522. {
  523. string[] errorsToSkip =
  524. {
  525. "Could not initialize audio output.",
  526. "Failed to create input layout for shader",
  527. "Failed to create texture"
  528. };
  529. foreach (var item in errorsToSkip)
  530. if (exc.Message.StartsWith(item))
  531. return;
  532. if (exc.Message.StartsWith("Failed to add resource path") ||
  533. exc.Message.StartsWith("Failed to add resource package"))
  534. {
  535. string msg = exc.Message.Remove(exc.Message.IndexOf("',") + 1);
  536. string assetDir = msg.Remove(0, msg.IndexOf('\'') + 1);
  537. msg +=
  538. #if ANDROID
  539. $"\n Assets must be located in '/Assets/{assetDir}' with 'AndroidAsset' Build Action.";
  540. #elif iOS
  541. $"\n Assets must be located in '/Resources/{assetDir}' with 'BundleResource' Build Action.";
  542. #elif WINDOWS_UWP || UWP_HOLO
  543. $"\n Assets must be located in '/{assetDir}' with 'Resource' Build Action";
  544. #else
  545. $"\n Assets must be located in '/{assetDir}'";
  546. #endif
  547. exc = new Exception(msg);
  548. }
  549. var args = new UnhandledExceptionEventArgs(exc);
  550. UnhandledException?.Invoke(null, args);
  551. if (!args.Handled && !isExiting)
  552. throw exc;
  553. }
  554. public static event EventHandler<UnhandledExceptionEventArgs> UnhandledException;
  555. class DelayState
  556. {
  557. public float Duration { get; set; }
  558. public TaskCompletionSource<bool> Task { get; set; }
  559. }
  560. }
  561. public class UnhandledExceptionEventArgs : EventArgs
  562. {
  563. public Exception Exception { get; private set; }
  564. public bool Handled { get; set; }
  565. public UnhandledExceptionEventArgs(Exception exception)
  566. {
  567. Exception = exception;
  568. }
  569. }
  570. }