Sharing with you mine and IMGN.PRO struggles of preparing Kholat on Nintendo Switch.
Starting with just a little knowledge of the Switch porting process, it took us about 10 months with a team of 3 individuals - a programmer, technical artist and a tester. No outsourcing, everything done "in-house".
As a base we decided to use the XBOX version of a game, as it was the last Kholat iteration that was made.
To be able to build for Switch platform it was necessary to migrate from version 4.16 up to 4.22 (which was later updated to 4.23 and finally 4.24).
API, that Kholat is using, had only slightly changed, so the decision was made to do it in one jump directly without intermediate engine versions.
This leap of faith left us with few assets being "broken", which required more effort in restoring to a version that resembles available version.
After the finish line, it is clear that even though a risky move, this approach saved us some time.
Despite Unreal Engine 4 doing on default much of the work, which helped us tremendously with build-in Switch support, we still had plenty of challenges ahead.
At the beginning we tentatively (with a bit of naivety) built and run Kholat on desired platform. At this point we were able to achieve utmost of 5 FPS - which wasn't a big shock, but merely a starting point.
Visuals, even if really pleasant for the eye, consumed great amount of processing power, leaving us with something totally unplayable.
Following path of many before us and studying other game porting cases, it was time to do some heavy work.
Original game was using dynamic lightning a lot, so it was the first thing disabled in exchange for statically baked lights.
Building lighting might be very time consuming process, so we minimized volumetric lighting and actual shadow resolutions to get some reasonable baking time with shadowmaps fitting memory budget.
To gain some of computing resources Tick was disabled on actors, that didn't require it at given time and place on scene i.e. like map found at the end of Act I.
Soon after we knew that CPU based calculation was fitting well in platform constraints, we moved to another, larger part of trimming - graphics.
Lowering LODs of every static geometry gave us another boost, but some of those required retopology - as auto LOD simplification pruned opened meshes and left some visible holes.
It is very important to create closed meshes, manifold geometry would be perfect.
We gained some performance when SSAO was disabled, putting us closer to 20-30 FPS. Generally materials were trimmed, to follow some strict regimes of instruction count and we weighted complexities of such visuals against overall graphic conception.
Enabling dynamic resolution scaling allowed us to reach desired frame capacity. From now on we measured performance with percentage of base resolution, but still there were places where frames tended to drop below expected count of 30.
Postproccess was cut down mercilessly, especially effects like Eye Adaptation and Depth of Field - in the same time tweaking what was left to compensate, trying to restore juiciness of image.
Quality of shader effects was also lowered. Translucency was almost entirely removed and replaced with dithering from anything that was possible starting from dropping snowflakes and quantity of other translucent meshes like fogs, hazes were minimized.
We iterated trimming process to achieve memory consumption of no more than ~2.3 gigs to leave some space for streaming levels.
When it comes to memory, we quickly experienced differences between stock Switch and development kit which comes with additional RAM. Testing for a target 4 GB (actual 3.1 GB of usable memory) became main priority.
Lowering every mipmap level of existing textures (except sky and few other) to a level where memory gain was huge but still looked OK, did the trick. Slowly moving toward targeted memory capacity, we narrowed streaming pool size and possible every other memory allocations.
We also tried to use ASTC texture format, but many artifacts appeared on baked shadowmaps, trying to remove those (which took us some time to understand real nature of it) put us back into DXT format, so no additional texture compression was possible without quality loss.
Original strategy of streaming on Act 2 was to load every sublevel and show/hide it with streaming volumes. After many random crashes, due to running out of memory, it was clear its simply impossible to stick to that, so we changed it into some loading sections. The game comes to a small halt there, approximately 1-3 seconds, the loading symbol appears and in this time level sections are loaded. This freed us from crashes and gave some additional memory we could use to maintain some of texture resolutions in exchange of players patience. Due to the fact that Act II has an open-world structure, we didn't want to chop that into smaller levels, as we wanted to remain the integrity of that world feeling.
Additionally, ghosts that we encounter during gameplay were striped of unused mesh parts, which were trickily hidden by masking out UV with unnecessary textures (hey, every megabyte of memory counts!).
Foliage was the next on the list. Huge amount of trees and plants were another thing that Nintendo Switch choked on. Trimming slightly actual quantity of nature did a little in a fight for millisecond of valuable computing time. Final speed up was gained when we prepared 2 groups of materials - first is using wind effect on assets made by speed tree and second is not. No wind materials were assigned to lower LODs of foliage, so that only LOD0 and LOD1 are bending and respond to atmospheric behaviour.
In the same time, we were tackling localizations - language translations were broken and missing some translations, selectively whole language fonts. Fixing that, was very consuming and forced us to spend more time than expected. It appears that UE4 made some changes in a way that translations behave asset-wise, so by trials and errors we finally managed to get it working expanding the possibility to enjoy the game in 14 different languages.
One trick we used for third Act, was to force post process quality to a higher level to maintain the quality.
TL;DR / the effect.
After 10 months of attacking various difficulties, and a LOT of bigger and smaller tasks, we are finally able to ship a game that works both in docked and handheld mode, with base resolution of 1080p and 720p respectively and both using dynamic resolution to stick to 30 FPS for practically entire time. Visuals still being pleasant for the eye and performance dropdowns going almost unnoticed. The loading times after deaths are fairly quick, varying between 5-10 seconds and fast travel loads are taking even less time. The longest times of loads are in-between the three acts, which take up to 25 seconds - thankfully, you will experience them only three times, that is if you do the whole play-through!