콘텐츠로 이동

Unity 6 iPadOS에서 RenderExtraFrameOnPause가 동작하지 않는 문제

이 문제는 Unity 6000.1.8f1 이상 버전에서 모두 발생한다(릴리스 노트).

참고한 Unity iOS 트램폴린 코드는 Editor\Data\PlaybackEngines\iOSSupport\Trampoline\Classes\UnityAppController.mm(6000.1.12f1)이다.

- (BOOL)shouldUseMetalDisplayLink
{
#if UNITY_USES_METAL_DISPLAY_LINK
if (@available(iOS 17.0, tvOS 17.0, *))
{
#if PLATFORM_TVOS
// most tvos devices are quite slow, and they can't keep up for complex scenes
// alas that might result in GPU hangs/errors which are hard to debug
// so by default we disable CAMetalDisplayLink on tvos
// but you can enable it and check that it works (also, it might make sense to enable to higher-end tvos only)
return NO;
#elif PLATFORM_VISIONOS
// not supported for now (but will be at some point)
return NO;
#else
return YES;
#endif
}
#endif
return NO;
}

iOS 17 이상 iPhone·iPad에서는 shouldUseMetalDisplayLink가 YES를 반환한다.

shouldUseMetalDisplayLink가 항상 NO를 반환하도록 iOS 후처리 빌드 스크립트(PostProcessBuild)에 함수를 추가했다.

[PostProcessBuild(2)]
public static void OnPostProcessBuild(BuildTarget target, string path)
{
if (target == BuildTarget.iOS)
{
string sourcePath = Path.Combine(SOURCE_FOLDER_ROOT, SOURCE_FOLDER_NAME);
string targetPath = Path.Combine(path, Path.Combine(XCODE_IMAGES_FOLDER, SOURCE_FOLDER_NAME));
FileUtil.DeleteFileOrDirectory(targetPath);
FileUtil.CopyFileOrDirectory(sourcePath, targetPath);
string projectPath = PBXProject.GetPBXProjectPath(path);
PBXProject project = new PBXProject();
project.ReadFromString(File.ReadAllText(projectPath));
// Add dynamic framework
string basePath = Application.dataPath + "/";
string frameworkPath = "Adiscope/Plugins/iOS/";
string[] arrFrameworks = {/*"Adiscope.framework"*/};
//var defaultTarget = project.TargetGuidByName(PBXProject.GetUnityTargetName());
var defaultTarget = project.GetUnityMainTargetGuid();
foreach (var framework in arrFrameworks)
AddEmbeddedFramework(ref project, defaultTarget, basePath + frameworkPath + framework, frameworkPath + framework);
// Unity 2021 대응 ( ERROR ITMS-90206 )
foreach (var targetGuid in new[] { defaultTarget, project.GetUnityFrameworkTargetGuid() })
project.SetBuildProperty(targetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "NO");
project.SetBuildProperty(defaultTarget, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
project.WriteToFile(projectPath);
foreach (var framework in arrFrameworks)
{
string contents = File.ReadAllText(projectPath);
string pattern = "(?<=Embed Frameworks)(?:.*)(\\/\\* " + framework + "\\ \\*\\/)(?=; };)";
string oldText = "/* " + framework + " */";
string updatedText = "/* " + framework + " */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }";
contents = Regex.Replace(contents, pattern, m => m.Value.Replace(oldText, updatedText));
File.WriteAllText(projectPath, contents);
}
// AddPPSDKCoreKitBundle(project, defaultTarget, projectPath);
AddProjectSettings(path);
// addd to plist
AddInfoPlist(path);
//add to capabilities
AddCapabilitiesBuildSetting(path);
ModifyUnityAppControllerForPortrait(path); // UnityAppController.mm 수정 : 외부 SDK와 Adiscope에서 Portrait모드 지원,
ForceReturnNoForShouldUseMetalDisplayLink(path); // UnityAppController.mm 수정 : iPadOS에서 shouldUseMetalDisplayLink가 YES로 설정되면 RenderExtraFrameOnPause가 동작하지 않아 항상 NO를 반환하도록 수정했습니다.
}
}
/*
* TODO: unity 업데이트로 버그 수정되면 코드 및 호출부 제거하기
* iPadOS에서 shouldUseMetalDisplayLink가 YES로 설정되면 RenderExtraFrameOnPause가 동작하지 않아 항상 NO를 반환하도록 수정했습니다.
* (unity 6000.1.8f1 버전 이상부터 발생)
*/
public static void ForceReturnNoForShouldUseMetalDisplayLink(string path)
{
string unityAppControllerPath = Path.Combine(path, "Classes/UnityAppController.mm");
if (!File.Exists(unityAppControllerPath))
{
Debug.LogWarning("UnityAppController.mm 파일을 찾을 수 없습니다: " + unityAppControllerPath);
return;
}
string content = File.ReadAllText(unityAppControllerPath);
string funcSignature = "- (BOOL)shouldUseMetalDisplayLink";
int funcIdx = content.IndexOf(funcSignature);
if (funcIdx < 0)
{
Debug.LogWarning($"{funcSignature} 함수 정의를 찾을 수 없습니다.");
return;
}
int braceIdx = content.IndexOf("{", funcIdx);
if (braceIdx < 0)
{
Debug.LogWarning($"{funcSignature} 함수의 '{{' 위치를 찾을 수 없습니다.");
return;
}
// 중첩 괄호 카운팅해서 함수의 끝 위치(정확한 닫는 중괄호)를 찾는다
int curIdx = braceIdx + 1;
int openBraces = 1;
while (curIdx < content.Length && openBraces > 0)
{
if (content[curIdx] == '{') openBraces++;
else if (content[curIdx] == '}') openBraces--;
curIdx++;
}
int funcEndIdx = curIdx - 1; // 함수의 닫는 중괄호 위치
// 함수 전체를 return NO;로 대체하고, 닫는 중괄호도 포함
string newFuncBody = "\n return NO;\n}";
string newContent = content.Substring(0, braceIdx + 1) + newFuncBody + content.Substring(funcEndIdx + 1);
File.WriteAllText(unityAppControllerPath, newContent);
Debug.Log("shouldUseMetalDisplayLink 함수가 강제로 return NO; 하도록 수정되었습니다.");
}

이후 Unity 측에서 Metal Display Link(CAMetalDisplayLink) 구현이 Metal 기기에서 화면 깜박임(delta time에 영향)과 멈춤을 일으킬 수 있다고 공유하고, Metal Display Link를 끄고 기존 Display Link(CADisplayLink)를 쓰도록 권장했다. 함께 안내받은 내용으로, iOS 26에서 기존 Display Link를 쓰면 앱을 백그라운드로 보냈다가 복귀할 때 30FPS로 고정되는 결함이 보고되어 있다. 앱의 그래픽 시스템은 ApplicationPause 해제 후 Application.targetFrameRate를 다시 설정하고 있어 이 30FPS 고정 문제는 겪지 않았다.

다만 위 후처리 스크립트는 Unity 가이드대로 바꿨다. UnityAppController.mm의 함수 본문을 강제로 NO 반환으로 바꾸던 방식 대신, UnityAppController.h의 define 값을 0으로 바꿔 shouldUseMetalDisplayLink가 NO를 반환하게 한다.

/*
* Metal Display Link를 비활성화하고 이전 Display Link 사용
* iOS에서 백그라운드 복귀 후 30FPS로 고정되는 버그 방지를 위한 예방 조치
*/
public static void DisableMetalDisplayLink(string path)
{
string unityAppControllerHeaderPath = Path.Combine(path, "Classes/UnityAppController.h");
if (!File.Exists(unityAppControllerHeaderPath))
{
Debug.LogWarning("UnityAppController.h 파일을 찾을 수 없습니다: " + unityAppControllerHeaderPath);
return;
}
string content = File.ReadAllText(unityAppControllerHeaderPath);
// Metal Display Link 정의 찾기
string oldDefine = "#define UNITY_USES_METAL_DISPLAY_LINK (UNITY_HAS_IOSSDK_17_0 || UNITY_HAS_TVOSSDK_17_0)";
string newDefine = "#define UNITY_USES_METAL_DISPLAY_LINK 0";
if (content.Contains(oldDefine))
{
content = content.Replace(oldDefine, newDefine);
File.WriteAllText(unityAppControllerHeaderPath, content);
Debug.Log("UNITY_USES_METAL_DISPLAY_LINK가 0으로 설정되어 Metal Display Link가 비활성화되었습니다.");
}
else if (content.Contains(newDefine))
{
Debug.Log("UNITY_USES_METAL_DISPLAY_LINK가 이미 0으로 설정되어 있습니다.");
}
else
{
Debug.LogWarning("UNITY_USES_METAL_DISPLAY_LINK 정의를 찾을 수 없습니다. Unity 버전이나 헤더 파일 구조가 변경되었을 수 있습니다.");
}
}

영향 버전과 기본 비활성화 버전

섹션 제목: “영향 버전과 기본 비활성화 버전”

CAMetalDisplayLink 지원은 2022.3.67f1과 6000.0.56f1에서 추가됐고, Xcode 15 이상으로 빌드해 iOS 17 이상에서 실행하면 기본으로 켜진다(2022.3.67f1 릴리스 노트, 6000.0.56f1 릴리스 노트). Unity에서 안내받은 영향 버전은 다음과 같다.

  • 2022.3.67f1
  • 6000.0.56f1 이후
  • 6000.1.8f1 이후
  • 6000.2.0a4 이후
  • 6000.3 이후

Unity는 근본 수정 전까지 CAMetalDisplayLink를 기본으로 끄기로 했다. Unity 이슈 트래커 기준으로 기본 비활성화가 들어간 버전은 2022.3.69f1, 6000.0.62f1, 6000.2.11f1, 6000.3.0b10, 6000.4.0a5이고, 6000.1은 지원이 끝나 수정하지 않는다(Unity Issue Tracker, 6000.0.62f1 릴리스 노트, 2022.3.69f1 릴리스 노트). 이 버전보다 낮은 버전을 쓰면 위 후처리 스크립트로 직접 꺼야 한다.

Unity 6에서 RenderExtraFrameOnPause가 제대로 동작하지 않는 이유는 새 렌더링 동기화 방식인 CAMetalDisplayLink 도입 때문이다.

Unity 6 코드는 CAMetalDisplayLink를 쓰는 경우, 앱이 비활성화될 때 추가 프레임을 렌더링하는 로직을 의도적으로 건너뛰도록 바뀌었다. iOS 17 이상 iPad에서는 이 방식을 쓰므로 Unity 2022에서 동작하던 기능이 Unity 6에서는 실행되지 않는다.

applicationWillResignActive: 비교 (Unity 2022.3.64f1 vs Unity 6000.1.12f1)

섹션 제목: “applicationWillResignActive: 비교 (Unity 2022.3.64f1 vs Unity 6000.1.12f1)”

차이는 앱이 비활성화될 때 호출되는 applicationWillResignActive: 내부에 있다.

Unity 2022:

#if UNITY_SNAPSHOT_VIEW_ON_APPLICATION_PAUSE
// Force player to do one more frame...
UnityWillPause();
[self repaint];
UnityWaitForFrame();
[self addSnapshotViewController];
#endif
UnityPause(1);

Unity 6:

#if UNITY_SNAPSHOT_VIEW_ON_APPLICATION_PAUSE
// we cannot repaint without drawable...
// TODO: there should be a way to handle this somehow
if(!self.unityUsesMetalDisplayLink)
{
UnityWillPause();
[self repaint];
[self addSnapshotViewController];
}
#endif
#if PLATFORM_VISIONOS
// ... visionOS pause logic
#else
UnityPause(1);
#endif
섹션 제목: “1. unityUsesMetalDisplayLink 조건 추가”

Unity 6 코드의 가장 큰 변화는 if(!self.unityUsesMetalDisplayLink) 조건이 추가된 것이다.

  • CAMetalDisplayLink: iOS 17부터 도입된 디스플레이 링크 방식으로, Metal 렌더링을 화면 주사율에 맞춰 더 효율적으로 동기화한다. Unity 6은 이 방식을 지원한다.
  • 코드의 의미: “CAMetalDisplayLink를 쓰고 있다면 추가 프레임을 그리는 로직(UnityWillPause, repaint)을 실행하지 말라”는 뜻이다.
  • 주석: 코드 주석에도 // we cannot repaint without drawable given to us by CAMetalDisplayLink(CAMetalDisplayLink가 드로어블을 주지 않으면 리페인트할 수 없음)라고 적혀 있다. 앱이 비활성화되는 시점에는 이 방식으로 추가 렌더링을 할 수 없다는 기술적 한계를 뜻한다. // TODO: 주석에는 이 경우를 처리할 방법이 있어야 한다고 적혀 있다.

Unity 2022 코드에는 [self repaint]; 다음에 UnityWaitForFrame();이 있다. 렌더링 스레드가 해당 프레임을 다 그릴 때까지 기다리는 동기화 코드다. 이 코드가 있어야 OS가 스크린샷을 찍기 전에 일시정지 화면 렌더링이 끝난다.

Unity 6에서는 if 블록 안에서도 UnityWaitForFrame()이 빠졌다. CAMetalDisplayLink를 쓰지 않아 추가 프레임을 그리는 경로에서도, 그 프레임의 렌더링이 끝나기를 기다리지 않고 다음 단계로 넘어간다. 이 문제의 직접 원인은 아니고, 추가 프레임이 스냅샷에 반영되는지는 따로 확인하지 않았다.

추가 프레임 로직 변경이 iPadOS 문제의 원인인 이유

섹션 제목: “추가 프레임 로직 변경이 iPadOS 문제의 원인인 이유”
  • Unity 2022는 기존 CADisplayLink 방식을 쓰고, applicationWillResignActive가 호출되면 항상 추가 프레임을 그리고 동기화까지 끝낸다.
  • Unity 6은 iOS 17 이상 iPhone·iPad에서 CAMetalDisplayLink를 기본으로 쓴다.
  • CAMetalDisplayLink를 쓰는 iPad에서 앱이 비활성화되면, 바뀐 코드에 따라 추가 프레임 렌더링 로직 전체가 건너뛰어진다.

Unity 6에서 내부 로직이 바뀌면서 기존 RenderExtraFrameOnPause 기능이 새 렌더링 파이프라인과 맞지 않게 됐다. Unity 2022에는 이 변경이 없어서 iPadOS에서도 정상 동작한다.

참고: 앱의 iOS 렌더링 설정 (Other Settings)

섹션 제목: “참고: 앱의 iOS 렌더링 설정 (Other Settings)”
  • Render Extra Frame on Pause: On
  • Multithreaded Rendering: On
  • Graphics Jobs: Off