[開發日誌][UnrealEngine] 關於在手機上使用MallocBinned的一些研究記錄

posted in: 開發日誌 | 0

在Unreal中MallocBinned總共有3個版本:MallocBinned、MallocBinne2、MallocBinned3。這類的memory pool機制,主要的設計原則就是希望盡量的將大小相同的物件放在一塊連續的記憶體上,然後根據物件的大小,一個蘿蔔一個坑,在分配的時候再從對應的Pool中拿出空間出來使用。通常我們只會對小物件做這件事,過大的記憶體物件需求通常會直接呼OS的allocator。 例如FMallocBinned所定義的小物件是以下幾個size:16, 32, 48, 64…到32768,也就是說至多到32KB。 看了一下android的memory pool機制,預設arm64是用MallocBinned2跑,而要不要換到MallocBinned3取決於各別專案會不會用爆目標最低機型的記憶體,會的話,由於MallocBinned3有Virtual Memory,因此可以突破記憶體上限。 另外IOS預設是使用MallocBinned,因為只有他有實作Apple系統內建的的nano allocator。系統的nano allocator預設會保留512MB的pool給small object用,看起來有點像是MallocBinned2在做的事;相對於MallocBinned的Pool,它是在該size被使用到的時候再去跟OS要一大塊記憶體回來。 關於nano allocator,詳細可以看Engine\Source\Runtime\Core\Private\Apple\ApplePlatformMemory.cpp中NanoMallocInit 的註解。以下是Copy出來的註解: 可以想見能在OS層級做掉的機制,當然會比FMallocBinned2引擎自己實作的還要高效,因此也不難理解iOS會選擇使用FMallocBinned來搭配。 IOS要換到MallocBinned3是不可能的,因為根本就沒有實作這條路,我有試著硬開出來,但會直接crash,微稍深入研究了一下Apple官方文檔,裡面有提到,雖然Mac相關VirtualMemory的功能都是正常的,但是我們只能page-in但不能page-out到硬碟上,不排除未來官方未來會在iOS上支援swap memory,可以觀察每年WWDC看看有沒有提到相關的議題,但現況就是iOS不支援MallocBinned3。另外有發現蘋果在發表iPadOS16的時候,有提到支援Virtual memory swap這個feature,這表示現況我們是能在iPad上使用MacllocBinned3的(待測)。 Apple官方文檔 iPadOS16 Feature介紹 只是為什麼iOS不支援MallocBinned3而Android支援? 我想是為了延長手機的生命週期吧。 由於MallocBinned3是依賴於VirtualMemory的技術,他會需要大量進行PageOut/PageIn的動作,但由於手機系統大多是使用FlashMemory,其在設計上有寫入次數的限制,寫入次數越多,其跟記憶體之間的資料交換會越不穩定。 因此iOS只支援PageIn而不支援PageOut的根本原因在此,為了維持系統的穩定度,減少硬碟的寫入次數,因此Apple禁止了PageOut這條路;由於不支援PageOut,因此我們就沒辦法使用MallocBinned3。 這個限制其實在Android系統上也是存在,只是該限制只存在於boot disk上,其他另外的secondary storage,例如SD card則沒有這種限制,所以Android還是可以正常的使用MallocBinned3。

[UnrealEngine][開發日誌] CommonUI plugin 筆記與 Plugin ( HorizonFramework ) 的設計思路

posted in: UnrealEngine, 開發日誌 | 0

關於Plugin的官方介紹: https://docs.unrealengine.com/5.0/en-US/common-ui-plugin-for-advanced-user-interfaces-in-unreal-engine/ 本篇文章主要是記錄目前我在CommonUI plugin中所看到的一些東西,可能分析的不是很完整,但就是一個簡單的筆記。 版本:Unreal 5.0.3 目前plugin裡面主要有以下幾個功能: 各別平台的Input以及 Icon Mapping:例如xbox跟switch的A跟B是相反的,他有機制處理輸入跟icon的對應。 Styling: 例如CommonText可以做一個Style BP給大家用,之後調整遊戲文字就只需要調該style BP就行了 從GameViewportClient上做Input Rereroute,根據目前是ECommonInputMode::Menu、Game或All決定裡面的行為。在功能面上看起來跟InputMode在機制上類似,Menu對應UIOnly、Game對應GameOnly而All對應GameAndUI,不確定之後官方會想怎麼發展。 UI Manager的機制。 我自己看是覺得整套機制還不是很成熟,在使用上你會覺得缺少很多細節的實作。不過也對,他現在是放在experimental plugin下面,要使用在production上要有心理準備自己需要去補完很多缺少的實作。 關於UI Manager,他實現的機制比較特別,我自己的Plugin是用Actor,但他是拿你第一個AddToViewport的主Widget來當管理器,這時候你會需要在這個Widget上再刻一套需要的功能介面好讓其他系統呼叫。 整套機制主要分為2個元素構成: Widget Container容器:分為UCommonActivatableWidgetStack跟UCommonActivatableWidgetQueue,分別可以進行push跟pop操作,這二個容器的差別在於我們的設計是先進先出或後進先出。我們可以有多個Stack分別對應不同的需求,在Lyra中他是把Game跟Popup Modal拆成二個stack ActivatableWidget:他是一個UserWidget,我們需要繼承下來進行設計的各種UI頁面,用來push進Widget Container的元素。。 他Push跟Pop時內建transition動畫,但他動畫效果是寫死在code中,而不是去呼叫UMG上的動畫,所以不是很符合實際產品製作的需求。內建的Transition動畫效果有:FadeOnly、Horizontal、Vertical、Zoom (ECommonSwitcherTransition)。 由於我以前有解決過UI Manager相關的問題,因此就再來多來聊聊我Plugin的設計思路吧。 以下是我Plugin的連結: Horizon Framework 這個Plugin當初設計的目的是想要做一套通用的遊戲框架(GameFramework),並做為HorizonDialogue擴建的基礎。只是做到後來發現通用框架難以在一個Plugin裡面實現出來,所以目前就變成了專注在UIManager相關的功能上面。 我的UI管理器做叫HorizonSceneManager,而用來進行Push跟Pop的元素叫做HorizonScene,Scene這個名稱是借鏡於cocos2d,他們都繼承自Actor,因此都會在World上生成一份實體。HorizonScene是用來封裝UserWidget,因此我們在設計完UserWidget之後,需要另外創建一份BP繼承自HorizonScene,並把UserWidget的Class指派進Scene的參數中。如下圖: SceneManager裡面有一份SceneStack跟SceneEventList,裡面有設計一系統的API讓我們可以針對SceneStack進行操作,幾個重要的API為:ChangeScene、PushScene、PopScene、RemoveScene。這幾個Function會建立對應的SceneEvent放進SceneEventList中,SceneManager會在每個Tick的時候逐一拿出來執行,並在適當的時機點通知SceneManager,並把Scene加入進SceneStack中、呼叫對應的delegate通知使用者實作需要的遊戲邏輯。 每個Scene主要會經過以下幾個生命週期: Enter:視是不是處於VR模式決定初始化Widget的方式,並呼叫StartTransIn。 … Continued

[UnrealEngine][開發日誌] 關於打包最後會出現的Waiting for child processes to complete訊息

posted in: UnrealEngine, 開發日誌 | 0

分享一下之前專案遇到的一個打包速度優化手段。 之前專案在打包的時候,最後一定出現Waiting for child processes to complete(0/1)這個訊息,當專案越變越大之後,很明顯的卡在這一步的時間變長了。追了一下code,發現他是在做把東西打包進pak的動作,每一個pak只會開一條thread處理,而訊息中的(0/1),則表示了我們把所有的內容塞進了同一個pak中(completed/numPak);也就是說若我們沒有啟用Chunk機制的話 (見:https://docs.unrealengine.com/4.27/en-US/SharingAndReleasing/Patching/GeneralPatching/CookingAndChunking/ ),這邊會浪費不少時間在做單核cpu的等待,之前試過,隨便分個8個chunk,就讓整體打包時間加快大約20分鐘左右。 通常會關注到chunk議題,很多時候是因為在做手遊時需要去分第一包跟做patch;但其實分chunk之後帶來的好處還蠻多的,除了打包速度加快之外,我們還可以利用UAssetManager來做Preload相關的機制。 關於打包的細節有興趣可以去看UnrealEngine\Engine\Source\Programs\AutomationTool\Scripts\CopyBuildToStagingDirectory.Automation.cs中的RunUnrealPakInParallel這個function。

[UnrealEngine][開發日誌] Shadre pipeline:Usage Flags vs ShadingModel

posted in: UnrealEngine, 開發日誌 | 1

UsageFlag文檔:https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/Materials/MaterialProperties/ ShadingModel文檔: https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/Materials/MaterialProperties/LightingModels/ 本文章是基於4.27 之前有空想要比對一下mobile pass跟SM5 pass到底差在哪裡。有發現在windows editor中,只有SM5跟ES3二種,就算是選了蘋果icon也不是真的metal,真正的metal shader code只能在mac環境下編譯出來。 在追code的過程中相關些名詞一直出現,引起了我的好奇,因此決定仔細研究一下。想要追shader相關的程式碼,這幾個ini參數最好設一下會比較好debug: 在material上有usage跟shading model的選項,usage主要的用途是用來決定哪些shader組合是不是要compile permutation;ShadingModel主要是用來決定計算光源貢獻時要走哪條IntegrateBxDF的路。 在Unreal中shader code的generation主要是靠C++ Template的機制來大量產生各種組合(permutation),其中可以看到引擎大量的使用Policy-Based Design。我們知道template的機制是compile time產code,所以我們只要有n個base shader,再加上m種policy的話,我們就可以產生n*m種組合。 每個Policy都會實作ShouldCompilePermutation跟ModifyCompilationEnvironment這二個function;前者搭配Usage使用,替BaseShaderType決定要搭配哪些Policy組合的編譯,例如,NoLightMapPolicy,接著實作會呼叫ModifyCompilationEnvironment,他會替Shader加macro,決定某段shader code要不要留下來。 以bIsUsedWithStaticLighting為例,若該usage有勾選的話,在跑IMPLEMENT_SHADER_TYPE時,其相關連到一組的BasePassVertexShader.usf、BasePassTessellationShaders.usf以及BasePassPixelShader.usf(Vertex Shader, Hull and Domain Shader, Pixel Shader)就會被用搭配到的TLightMapPolicy建立出來。 例如在${PROJECT_ROOT}/Saved/ShaderDebugInfo/PCD3D_SM5/AmbientOcclusion/FLocalVertexFactory可以看到類似於下面中的資料夾。其中產生的permutation shader code,針對BasePassVertexShader.usf,產生NoLightMapPolicy以及NoLightMapPolicyAtmosphericFog這二組;針對BasePassPixelShader.usf,則產生了FNoLightMapPolicy跟FNoLightMapPolicySkylight。 這些Policy主要的功能是用來ModifyCompilationEnvironment,替shader code加入需要的macro define,之後每個permutation就會利用mcpp這個preprocess來幫我們濾掉不需要code,並把所有include到的shader結合成一個大檔之後,產生特製的shader code出來。 另外Usage除了決定permutation要不要產生出來外,有一些還會用來附加一些macro進shader中,例如OutEnvironment.SetDefine(TEXT(“USES_DISTORTION”), Material->IsDistorted()); 有發現Usage雖然有八成左右的功能都只是用來決定編譯組合;ShadingModel主要是用來決定計算光源貢獻的做法。但有看到有少部份會直接去加shader … Continued

[UnrealEngine][開發日誌] Shadre pipeline optimization: 關於VerifyGlobalShaders的實驗結果令我非常震驚

posted in: UnrealEngine, 開發日誌 | 0

之前在debug android的時候,有發現一開始的黑畫面特別久,特別在用Visual Studio attach debuger的時候,基本上第一畫面完全跑不出來。好奇追了一下,發現了整個流程是卡在VerifyGlobalShaders這個function上,仔細看了裡面,發現在package build的時候他完全沒做任何事,就只是把所有的shader的permutation撈出來for loop跑過一輪檢查而已,這個foreach的執行次數在4.27有8萬多個(85610),看起來非常浪費計算量,就算是跳過也不會影響後面的功能,說不定跳過之後能夠省個幾秒鐘的黑畫面時間。試了一下,這個想法是對的,跳過之後確實後面的畫面也能夠正常顯示出來,讓我的debugger能夠順利的往後執行。 但這時候我就好奇了,為什麼引擎要放一個明顯無用的ForLoop在那裡?這段code通常只會在Development build執行吧?為什麼連Shipping build也要跑?把他跳過之後我們能夠省下多少時間? 為了驗證這個更動能夠帶來更好的遊戲體驗,因此我決定計時看看跳過VerifyGlobalShaders 跟原本不跳過的版本差異在哪。使用的版本是Test並啟用了PGO優化。 但是,結果讓我非常的震驚,數據如下。 機型:Galaxy S21 5G SM-G9910   Skip NoSkip 第一次開啟 12.52秒 7.9秒 第二次開啟 1.87秒 1.66秒 機型:Galaxy S21 ultra 5G   Skip NoSkip 第一次開啟 10.28秒 7.98秒 第二次開啟 2.13秒 1.71秒 這個驚人反直覺的結果,讓我開始思考到底發生了什麼事。為什麼跳過無用的Loop,其執行結果反而比沒跳過的慢?這之間是不是有什麼硬體上的預熱效果在裡頭發揮了作用?思考到這裡,我突然想到,所有的CPU都有所謂的L1、L2 … Continued

[UE4][開發日誌]使用nuget來發佈遊戲專案中的UE4Editor dll

posted in: UnrealEngine, 開發日誌 | 0

這幾天花了一此時間研究了nuget的佈署方式,主要的目的,便是要讓大家在還真的購買plugin之前,有個機會先能夠在editor中進行試用。 雖然perforce使用者有[UGS](https://docs.unrealengine.com/en-us/Programming/Deployment/Unreal-Game-Sync/Unreal-Game-Sync-Overview)可以用,但由於跟perforce綁定,對於習慣於用git的自己而言還是要去思考別的替代方案。 另外,nuget好處是,開放給大家的部份可以發到nuget.org上,如果公司內部要私用的話,也是可以自己架一個nuget server或者是直接弄一個local feed在NAS上,就使用彈性而言,我個人覺得比UGS來得高。 目前我正在開發的幾個plugin都也已經發佈到nuget.org開發大家下載了,有興趣的人可以試試看,使用方法跟步驟已經記錄在對應的Github頁面中了。 plugin marketplace: https://www.unrealengine.com/marketplace/profile/horizon-studioGithub專案連結:https://github.com/dorgonman/HorizonUIPluginDemohttps://github.com/dorgonman/HorizonTweenDemohttps://github.com/dorgonman/HorizonFrameworkDemohttps://github.com/dorgonman/HorizonVRDemohttps://github.com/dorgonman/HorizonDialogueDemo 另外由於我這幾個專案都有進Azure Devops的build pipeline了,理論上只要我這邊有任何更新,都會自動發佈到nuget.org上。

[UE4開發日誌]VSTS CI Build與UE4 Automation Test整合

posted in: 開發日誌 | 0

剛剛成功將UE4的Test Automation的功能跟VSTS的CI build整合在一起了。總算能夠處理Test失敗的狀況並回報建置錯誤。雖然D3D Device抓不到只能用server mode跑,但基本上要做funtional test跟unit test看起來沒什麼問題。之後測試好好寫的話,看起來可以省掉不少做回歸測試的功夫。

[UE4]HorizonTweenPlugin新功能:MultiTween Event(ETA 4.19)

posted in: 開發日誌 | 0

這個週末為HorizonTweenPlugin添加了新功能:MutiTween Event。 這個功能有什麼用處呢? 之前這個Plugin想要一次做旋轉跟移動的TweenEvent的時候,我們需要各別為該物件各別添加Move、Rotate跟Scale…等等,用久了總覺得哪裡彆扭,而且BP Node越來越多看起來實在是很不舒爽。 這次實作的功能,讓我們可以很方便的在一個BP Node中一次加入多個Tween事件啦! 不多說,讓我們來看看展示影片,預計UE4.19時更新:https://www.youtube.com/watch?v=Tg0sqlCbAHU&feature=youtu.be

[UE4]UWorld與UGameInstance初始化

posted in: UnrealEngine, 開發日誌 | 2

今天有使用者在使用我的HorizonTween這套Plugin時發生了所綁定的回呼事件無法觸發的問題。深入去了解之後,發現了一個有趣的現象。 首先,他在UMyGameInstance::Init中做了類似於下面的回呼事件的綁定: 在遊戲開始後,期待著每次呼叫建立TweenEvent之後,就會回呼到這個事件中可以做一些遊戲邏輯的處理。 看起來這個劇本非常的完美,在Editor中測試也完美的運行,但是當我們實際上打包出來後就會發現:為什麼這邊的回呼綁定沒有被執行!? 首先,這邊的UHorizonTweenSystemLibrary::GetDefaultTweenSystem(this),其所產生的TweenSystem Actor,其實是跟著UWorld的生命週期走的,也就是說,當UWorld物件被引擎回收(Garbage Collection)時,這邊的TweenSystem Actor也會跟著被回收掉。當我們實際在UMyGameInstance::Init中下斷點後,就會發現一件有趣的事:   我們可以看到World所連結到的Map名字是Untitled,這代表什麼意思?也就是說,在UMyGameInstance::Init這個時間點,我們都還是處在引擎所暫時產生出來的『transition map』中,這個level所產生出來的world在實際的map載入之後就會被移除掉,意即剛剛在UMyGameInstance::Init所生成的任何Actor物件都會被馬上回收。 下面這張圖則是在LevelBlueprint中呼叫,我們可以發現,這次我們所產生的Actor就會存在於「正確」的world當中了(HorizonTweenDemo)。 因此我們知道: 千萬不能在UMyGameInstance::Init中Spawn任何的Actor,因為是沒用的,馬上就會被回收掉!

[UE4] HorizonDBPlugin update

posted in: UnrealEngine, 開發日誌 | 0

今天試著把之前做的UE4 DB Plugin在4.18上做了整修,之前是4.13做完之後就一直放著,目前總算是可以在4.18上運行了。基本上這套Plugin對soci( https://github.com/SOCI/soci )做了一層封裝,讓其可以在UE4中使用到其強大的ORM機制。重點是,在C++中宣告完類別之後,BP也能使用喔! 這套Plugin目前是免費開源狀態(Boost Software License – Version 1.0),有興趣的朋友趕快前往下載來試試吧:https://github.com/dorgonman/HorizonDatabaseDemo C++ UnitTest BP ScreenShot: https://github.com/dorgonman/HorizonDatabaseDemo/tree/master/Screenshot