UnityView.mm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. #include "UnityView.h"
  2. #include "UnityAppController.h"
  3. #include "UnityAppController+Rendering.h"
  4. #include "OrientationSupport.h"
  5. #include "Unity/DisplayManager.h"
  6. #include "Unity/UnityMetalSupport.h"
  7. #include "Unity/ObjCRuntime.h"
  8. extern bool _renderingInited;
  9. extern bool _unityAppReady;
  10. extern bool _skipPresent;
  11. @implementation UnityView
  12. {
  13. CGSize _surfaceSize;
  14. }
  15. @synthesize contentOrientation = _curOrientation;
  16. - (void)onUpdateSurfaceSize:(CGSize)size
  17. {
  18. _surfaceSize = size;
  19. CGSize systemRenderSize = CGSizeMake(size.width * self.contentScaleFactor, size.height * self.contentScaleFactor);
  20. _curOrientation = (ScreenOrientation)UnityReportResizeView((unsigned)systemRenderSize.width, (unsigned)systemRenderSize.height, _curOrientation);
  21. ReportSafeAreaChangeForView(self);
  22. }
  23. - (void)boundsUpdated
  24. {
  25. [self onUpdateSurfaceSize: self.bounds.size];
  26. }
  27. - (void)initImpl:(CGRect)frame scaleFactor:(CGFloat)scale
  28. {
  29. #if !PLATFORM_TVOS
  30. self.multipleTouchEnabled = YES;
  31. self.exclusiveTouch = YES;
  32. #endif
  33. self.contentScaleFactor = scale;
  34. self.isAccessibilityElement = TRUE;
  35. self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction;
  36. self.skipRendering = NO;
  37. #if UNITY_TVOS
  38. _curOrientation = UNITY_TVOS_ORIENTATION;
  39. #elif UNITY_VISIONOS
  40. _curOrientation = UNITY_VISIONOS_ORIENTATION;
  41. #endif
  42. [self onUpdateSurfaceSize: frame.size];
  43. }
  44. - (id)initWithFrame:(CGRect)frame scaleFactor:(CGFloat)scale;
  45. {
  46. if ((self = [super initWithFrame: frame]))
  47. [self initImpl: frame scaleFactor: scale];
  48. return self;
  49. }
  50. - (id)initWithFrame:(CGRect)frame
  51. {
  52. if ((self = [super initWithFrame: frame]))
  53. [self initImpl: frame scaleFactor: 1.0f];
  54. return self;
  55. }
  56. - (id)initFromMainScreen
  57. {
  58. #if !PLATFORM_VISIONOS
  59. CGRect frame = [UIScreen mainScreen].bounds;
  60. CGFloat scale = UnityScreenScaleFactor([UIScreen mainScreen]);
  61. #else
  62. CGRect frame = CGRectMake(0.0f, 0.0f, 1920.0f, 1080.0f);
  63. CGFloat scale = 1.0f;
  64. #endif
  65. if ((self = [super initWithFrame: frame]))
  66. [self initImpl: frame scaleFactor: scale];
  67. return self;
  68. }
  69. - (void)resumeRendering
  70. {
  71. self.skipRendering = NO;
  72. }
  73. - (void)layoutSubviews
  74. {
  75. if (_surfaceSize.width != self.bounds.size.width || _surfaceSize.height != self.bounds.size.height)
  76. _shouldRecreateView = YES;
  77. [self onUpdateSurfaceSize: self.bounds.size];
  78. for (UIView* subView in self.subviews)
  79. {
  80. if ([subView respondsToSelector: @selector(onUnityUpdateViewLayout)])
  81. [subView performSelector: @selector(onUnityUpdateViewLayout)];
  82. }
  83. [super layoutSubviews];
  84. }
  85. - (void)safeAreaInsetsDidChange
  86. {
  87. ReportSafeAreaChangeForView(self);
  88. }
  89. - (void)recreateRenderingSurfaceIfNeeded
  90. {
  91. #if !PLATFORM_VISIONOS
  92. float requestedContentScaleFactor = UnityScreenScaleFactor([UIScreen mainScreen]);
  93. #else
  94. float requestedContentScaleFactor = 1.0f;
  95. #endif
  96. if (abs(requestedContentScaleFactor - self.contentScaleFactor) > FLT_EPSILON)
  97. {
  98. self.contentScaleFactor = requestedContentScaleFactor;
  99. [self onUpdateSurfaceSize: self.bounds.size];
  100. }
  101. unsigned requestedW, requestedH; UnityGetRenderingResolution(&requestedW, &requestedH);
  102. int requestedMSAA = UnityGetDesiredMSAASampleCount(1);
  103. int requestedSRGB = UnityGetSRGBRequested();
  104. int requestedWideColor = UnityGetWideColorRequested();
  105. int requestedHDR = UnityGetHDRModeRequested();
  106. int requestedMemorylessDepth = UnityMetalMemorylessDepth();
  107. UnityDisplaySurfaceBase* surf = GetMainDisplaySurface();
  108. if (_shouldRecreateView == YES
  109. || surf->targetW != requestedW || surf->targetH != requestedH
  110. || surf->disableDepthAndStencil != UnityDisableDepthAndStencilBuffers()
  111. || surf->msaaSamples != requestedMSAA
  112. || surf->srgb != requestedSRGB
  113. || surf->wideColor != requestedWideColor
  114. || surf->hdr != requestedHDR
  115. || surf->memorylessDepth != requestedMemorylessDepth
  116. )
  117. {
  118. [self recreateRenderingSurface];
  119. }
  120. }
  121. - (void)recreateRenderingSurface
  122. {
  123. if (_renderingInited)
  124. {
  125. unsigned requestedW, requestedH;
  126. UnityGetRenderingResolution(&requestedW, &requestedH);
  127. RenderingSurfaceParams params =
  128. {
  129. .msaaSampleCount = UnityGetDesiredMSAASampleCount(1),
  130. .renderW = (int)requestedW,
  131. .renderH = (int)requestedH,
  132. .srgb = UnityGetSRGBRequested(),
  133. .wideColor = UnityGetWideColorRequested(),
  134. .hdr = UnityGetHDRModeRequested(),
  135. .metalFramebufferOnly = UnityMetalFramebufferOnly(),
  136. .metalMemorylessDepth = UnityMetalMemorylessDepth(),
  137. .disableDepthAndStencil = UnityDisableDepthAndStencilBuffers(),
  138. .useCVTextureCache = 0,
  139. };
  140. APP_CONTROLLER_RENDER_PLUGIN_METHOD_ARG(onBeforeMainDisplaySurfaceRecreate, &params);
  141. [GetMainDisplay() recreateSurface: params];
  142. // actually poke unity about updated back buffer and notify that extents were changed
  143. UnityReportBackbufferChange(GetMainDisplaySurface()->unityColorBuffer, GetMainDisplaySurface()->unityDepthBuffer);
  144. APP_CONTROLLER_RENDER_PLUGIN_METHOD(onAfterMainDisplaySurfaceRecreate);
  145. if (_unityAppReady)
  146. {
  147. // seems like ios sometimes got confused about abrupt swap chain destroy
  148. // draw 2 times to fill "both" buffers (we assume double buffering)
  149. // present only once to make sure correct image goes to CA
  150. // if we are calling this from inside repaint, second draw and present will be done automatically
  151. _skipPresent = true;
  152. // we may be asked to recreate surface while paused (in the background)
  153. // like changing device orientation while showing some system dialog
  154. // in this case we still want to redraw contents to avoid view stretching
  155. const bool wasPaused = UnityIsPaused();
  156. // please note that we still need to pretend we did come from displaylink to make sure vsync magic works
  157. // NOTE: unity does handle "draw frame with exact same timestamp" just fine
  158. UnityDisplayLinkCallback(GetAppController().unityDisplayLink.timestamp);
  159. UnityRepaint();
  160. // if we are inside actual repaint: we are done (second draw and present will be done automatically)
  161. // otherwise we need the second repaint, actualy doing present this time
  162. _skipPresent = false;
  163. if (_viewIsRotating || wasPaused)
  164. {
  165. UnityDisplayLinkCallback(GetAppController().unityDisplayLink.timestamp);
  166. UnityRepaint();
  167. }
  168. }
  169. }
  170. _shouldRecreateView = NO;
  171. }
  172. @end
  173. @implementation UnityView (Deprecated)
  174. - (void)recreateGLESSurfaceIfNeeded { [self recreateRenderingSurfaceIfNeeded]; }
  175. - (void)recreateGLESSurface { [self recreateRenderingSurface]; }
  176. @end
  177. static Class UnityRenderingView_LayerClassMTL(id self_, SEL _cmd)
  178. {
  179. return NSClassFromString(@"CAMetalLayer");
  180. }
  181. static Class UnityRenderingView_LayerClassNULL(id self_, SEL _cmd)
  182. {
  183. return NSClassFromString(@"CALayer");
  184. }
  185. @implementation UnityRenderingView
  186. + (Class)layerClass
  187. {
  188. return nil;
  189. }
  190. + (void)InitializeForAPI:(UnityRenderingAPI)api
  191. {
  192. IMP layerClassImpl = api == apiMetal ? (IMP)UnityRenderingView_LayerClassMTL : (IMP)UnityRenderingView_LayerClassNULL;
  193. class_replaceMethod(object_getClass([UnityRenderingView class]), @selector(layerClass), layerClassImpl, UIView_LayerClass_Enc);
  194. }
  195. @end
  196. void ReportSafeAreaChangeForView(UIView* view)
  197. {
  198. CGRect safeArea = ComputeSafeArea(view);
  199. UnityReportSafeAreaChange(safeArea.origin.x, safeArea.origin.y,
  200. safeArea.size.width, safeArea.size.height);
  201. #if !PLATFORM_VISIONOS
  202. if (UnityDeviceHasCutout())
  203. {
  204. CGSize cutoutSizeRatio = GetCutoutToScreenRatio();
  205. if (!CGSizeEqualToSize(CGSizeZero, cutoutSizeRatio))
  206. {
  207. const float w = ([UIScreen mainScreen].nativeBounds.size.width * cutoutSizeRatio.width);
  208. const float h = ([UIScreen mainScreen].nativeBounds.size.height * cutoutSizeRatio.height);
  209. // Apple's cutouts are currently centred on the horizontal, and stuck to the top of the vertical, hence this positioning.
  210. const float x = (([UIScreen mainScreen].nativeBounds.size.width - w) / 2);
  211. const float y = ([UIScreen mainScreen].nativeBounds.size.height - h);
  212. UnityReportDisplayCutouts(&x, &y, &w, &h, 1);
  213. return;
  214. }
  215. }
  216. #endif
  217. UnityReportDisplayCutouts(nullptr, nullptr, nullptr, nullptr, 0);
  218. }
  219. CGRect ComputeSafeArea(UIView* view)
  220. {
  221. CGSize screenSize = view.bounds.size;
  222. CGRect screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
  223. UIEdgeInsets insets = [view safeAreaInsets];
  224. float insetLeft = insets.left, insetBottom = insets.bottom, insetTop = insets.top;
  225. float insetHeight = insetBottom + insetTop;
  226. float insetWidth = insetLeft + insets.right;
  227. #if PLATFORM_IOS && !PLATFORM_VISIONOS
  228. // pre-iOS 15 there is a bug with safeAreaInsets when coupled with the way unity handles forced orientation
  229. // when we create/show new ViewController with fixed orientation, safeAreaInsets include status bar always
  230. // alas, we did not find a good way to work around that (this can be seen even in View Debugging: Safe Area would have status bar accounted for)
  231. // we know for sure that status bar height is 20 (at least on ios16 or older), so we can check if the safe area
  232. // includes inset of this size while status bar should be hidden in that case we reset top inset and keep
  233. // bottom one (might include home button swipe line,etc.).
  234. if (@available(iOS 15, *))
  235. {
  236. // everything works as expected
  237. }
  238. else
  239. {
  240. bool isStatusBarHidden = false;
  241. if (@available(iOS 13, *))
  242. isStatusBarHidden = view.window.windowScene.statusBarManager.statusBarHidden;
  243. else
  244. isStatusBarHidden = [UIApplication sharedApplication].statusBarHidden;
  245. if (isStatusBarHidden && fabsf(insetTop - 20) < 1e-6f)
  246. insetHeight -= insetTop;
  247. }
  248. #endif
  249. // Unity uses bottom left as the origin
  250. screenRect = CGRectOffset(screenRect, insetLeft, insetBottom);
  251. screenRect.size.width -= insetWidth;
  252. screenRect.size.height -= insetHeight;
  253. const float scale = view.contentScaleFactor;
  254. // Truncate safe area size because in some cases (for example when Display zoom is turned on)
  255. // it might become larger than Screen.width/height which are returned as ints.
  256. screenRect.origin.x = (unsigned)(screenRect.origin.x * scale);
  257. screenRect.origin.y = (unsigned)(screenRect.origin.y * scale);
  258. screenRect.size.width = (unsigned)(screenRect.size.width * scale);
  259. screenRect.size.height = (unsigned)(screenRect.size.height * scale);
  260. return screenRect;
  261. }
  262. // Apple does not provide the cutout width and height in points/pixels. They *do* however list the
  263. // size of the cutout and screen in mm for accessory makers. We can use this information to calculate the percentage of the screen is cutout.
  264. // This information can be found here - https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
  265. CGSize GetCutoutToScreenRatio()
  266. {
  267. switch (UnityDeviceGeneration())
  268. {
  269. case deviceiPhone14:
  270. case deviceiPhone16e:
  271. return CGSizeMake(0.415, 0.04);
  272. case deviceiPhone14Plus:
  273. return CGSizeMake(0.377, 0.036);
  274. case deviceiPhone14Pro:
  275. case deviceiPhone15:
  276. case deviceiPhone15Pro:
  277. case deviceiPhone16:
  278. case deviceiPhone16Pro:
  279. return CGSizeMake(0.318, 0.057);
  280. case deviceiPhone14ProMax:
  281. case deviceiPhone15ProMax:
  282. case deviceiPhone15Plus:
  283. case deviceiPhone16Plus:
  284. case deviceiPhone16ProMax:
  285. return CGSizeMake(0.292, 0.052);
  286. case deviceiPhone13ProMax:
  287. return CGSizeMake(0.373, 0.036);
  288. case deviceiPhone13Pro:
  289. case deviceiPhone13:
  290. return CGSizeMake(0.4148, 0.0399);
  291. case deviceiPhone13Mini:
  292. return CGSizeMake(0.4644, 0.0462);
  293. case deviceiPhone12ProMax:
  294. return CGSizeMake(0.4897, 0.0346);
  295. case deviceiPhone12Pro:
  296. case deviceiPhone12:
  297. return CGSizeMake(0.5393, 0.0379);
  298. case deviceiPhone12Mini:
  299. return CGSizeMake(0.604, 0.0424);
  300. case deviceiPhone11ProMax:
  301. return CGSizeMake(0.5057, 0.0335);
  302. case deviceiPhone11Pro:
  303. return CGSizeMake(0.5583, 0.037);
  304. case deviceiPhone11:
  305. case deviceiPhoneXR:
  306. return CGSizeMake(0.5568, 0.0398);
  307. case deviceiPhoneXSMax:
  308. return CGSizeMake(0.4884, 0.0333);
  309. case deviceiPhoneX:
  310. case deviceiPhoneXS:
  311. return CGSizeMake(0.5391, 0.0368);
  312. default:
  313. NSCAssert(!UnityDeviceHasCutout(), @"Device has a cutout, but no ratio has been added for it.");
  314. return CGSizeZero;
  315. }
  316. }