ValveSoftware / steam-runtime

A runtime environment for Steam applications
Other
1.18k stars 86 forks source link

Unity games depend on fonts not available in container #555

Open flibitijibibo opened 1 year ago

flibitijibibo commented 1 year ago

Your system information

depot 0.20221018
pressure-vessel 0.20221014.0
scripts v0.20220823.0-0-gcc4e44f
soldier 0.20221017.1

Please describe your issue in as much detail as possible:

Unity's font system has an unusual issue where, even when embedding fonts, the engine will still depend on files in /usr/share/fonts (and very specifically that directory, it's hardcoded in UnityPlayer.so!), and when the font is not found, Unity simply renders nothing with no warning or messages in the log whatsoever. Overall, a top tier font system.

Normally this isn't too bad, because most developers just use the Noto collection (particularly for CJK and Arabic) and just about everybody has that, but the Steam runtime is one notable exception.

I'm hoping to find a real solution to this (if Valve wants to bother any Unity employees it would help move this along for future releases), but for now the best fix for maximum compatibility is for the runtime to, at the very least, match the font collection found in SteamOS itself.

Steps for reproducing this issue:

  1. Switch to desktop mode on Steam Deck
  2. Install Superliminal (I swear we embedded the fonts, you can find them by grepping around for NotoSans in the assets...)
  3. Run the game from the Steam client, go to Settings -> Gameplay Settings -> Language dropdown
  4. Observe a really messed up language list!
  5. Close the game, open Konsole, and navigate to your Superliminal installation
  6. Run ./Superliminal, go to Settings -> Gameplay Settings -> Language dropdown
  7. Languages work now!
TTimo commented 1 year ago

This can be reproduced under gamescope as well. subliminal

smcv commented 1 year ago

Is it only the 3-4 languages rendered as blank or () between Russian and Polish that you are concerned about there, or are English, German etc. in your screenshot also considered to be rendered "wrong"? I don't have Superliminal myself, so I don't know what these would ideally look like on the developer's system.

pressure-vessel uses the same tricks as Flatpak to make the fonts from the host system available in the container and configure fontconfig to look there, but obviously if Unity Player doesn't use fontconfig and instead hard-codes /usr/share/fonts, then that's not going to work.

Overall, a top tier font system.

Indeed.

This seems like something that would be a problem even without the container runtime: there's no guarantee that users have specific fonts installed, or that OSs always install them in /usr/share/fonts, or that the exact filenames or font names in /usr/share/fonts are what a particular game would expect.

We could bundle a copy of Noto in the container runtime, but the space consumption is going to be unpleasant if we do that, particularly with both soldier and sniper:

$ du -sh /usr/share/fonts/*/noto
298M    /usr/share/fonts/opentype/noto
458M    /usr/share/fonts/truetype/noto

Adding those would more than double the size of each runtime (the release-candidates I tested most recently are 633M for soldier and 680M for sniper, production runtimes should be about that size too).

This might be a bit of an overestimate, because I'm on Debian 12, whereas sniper and soldier are based on Debian 11 and 10 respectively, which are a bit older and might have somewhat older/smaller/less fancy versions of Noto, but I would expect them to be of the same order of magnitude.

Obviously we could mitigate the space consumption concern by shipping a shared bundle of fonts in a suitable format in the Steam client or a common depot (like a font equivalent of Steam's "common redist"), and teach each container runtime to pick up that shared bundle from an environment variable set by Steam and mount it over the top of the runtime's fallback (or even empty) /usr/share/fonts, so that we only need one copy that all runtimes can share; but even if we do that, it's still costing us an amount of space comparable to one entire runtime, which doesn't seem great.

I'm also concerned that an Opentype|Truetype copy of Noto is an obvious and popular fallback font in 2023, but it's far from the first font I've seen suggested in that role. We currently ship a Truetype copy of Deja Vu (the community-maintained fork of Bitstream Vera), which was the obvious choice for a fallback font in the open source world before Noto became popular. Before Deja Vu, in turn, the obvious choice would have been a Truetype copy of Bitstream Vera itself; or maybe URW Nimbus, or Liberation, or Latin Modern, or Freefont, or GNU Unifont, and so on; in Truetype or even Type1.

Deja Vu has the advantage that the version we ship is 2.8M, which is 2 orders of magnitude smaller than Noto (although obviously its glyph support is correspondingly smaller, and in particular we almost certainly don't have CJK).

smcv commented 1 year ago

pressure-vessel uses the same tricks as Flatpak to make the fonts from the host system available in the container and configure fontconfig to look there

Specifically, if the host system has /usr/share/fonts, it appears at /run/host/fonts in the container. Similarly, /usr/local/share/fonts turns up at /run/host/local-fonts, and ~/.local/share/fonts or ~/.fonts at /run/host/user-fonts. Well-known font cache directories like /var/cache/fontconfig:/usr/lib/fontconfig/cache also get into the container. All of these paths are considered to be implementation details, and might change.

smcv commented 1 year ago

I'm also concerned that an Opentype|Truetype copy of Noto is an obvious and popular fallback font in 2023, but it's far from the first font I've seen suggested in that role

... which means I suspect that in 2025 or 2030, we might well be looking at Noto and thinking "why was this the default? and now we can't remove this for compatibility reasons", in the same way we might say the same about GNU Unifont today.

flibitijibibo commented 1 year ago

In this case we're only worried about the blank or () entries, though while verifying the issue I did notice on some other systems that the Arabic font would disappear as well. Unity is not very good at falling back (read: I don't think it does any fallbacks period), so if it renders at all then it found what it wanted.

One possibly obnoxious fix is something similar to the EasyAntiCheat runtime where Steam has a depot just for the fonts, and games can opt into them for use with the SLR on Deck in particular. That would limit the file size in exchange for needing extra work to mount it into the right place for Unity to find the files it wants.

And of course, the correct fix is for A: Unity to actually embed the fonts like it claims to, and B: actually use fontconfig. I dunno if they would backport these fixes to, say, 2019.4 LTS though.

smcv commented 1 year ago

One possibly obnoxious fix is something similar to the EasyAntiCheat runtime where Steam has a depot just for the fonts, and games can opt into them for use with the SLR on Deck in particular. That would limit the file size in exchange for needing extra work to mount it into the right place for Unity to find the files it wants.

Right, the Steam client would need to:

and then SLR would have to bind-mount it over /usr/share/fonts.

Another possible fix would be for SLR to stop doing its Flatpak-inspired font setup (which is to provide the soldier or sniper runtime's minimal fallback font set, currently Deja Vu, in /usr/share/fonts, and the system fonts in /run/host/fonts); and instead mount the system fonts on /usr/share/fonts. The failure mode there would be that if the host OS has something unreasonable/broken in /usr/share/fonts, we will try to use it, fontconfig will fail to load from the resulting directory, and you won't have any fonts at all (not even the Latin subset of Deja Vu) which seems pretty bad. I don't know what the heuristic would be for recognising the host /usr/share/fonts as reasonable or unreasonable.

I agree that having the game metadata opt-in to this seems less concerning than indiscriminately applying the same workaround to all games, Unity or otherwise; that way, anything that goes wrong with the workaround can only hurt the Unity games, which already have font problems.

It would be nice if we could at least ensure this workaround only applies to games that were built for the scout runtime (and run via the scout-on-soldier SLR environment), but right now that would have practical problems. Any workaround involving mounting different directories would have to be done in pressure-vessel (as part of soldier), but the way the Steam compat tool interface works, soldier doesn't actually know whether its "payload" is going to be a native soldier game, or Proton, or scout-on-soldier: all we are told is a command-line to run. We screen-scrape the command-line and make reasonable guesses at what it means for debug features like steam-runtime-launch-options, but I'm not comfortable with doing that for production code, because it's inherently fragile. I suppose we could have a heuristic to figure it out from $STEAM_COMPAT_TOOL_PATHS, but ugh.

I would definitely prefer this workaround to never be used for sniper games like Retroarch and (a non-default branch of) Battle for Wesnoth, because those are our opportunity to draw a line between the long tail of backwards-compatibility, and the container-friendly future.

smcv commented 1 year ago

We already have STEAM_COMPAT_FLAGS which is intended to be set by Steam, and is a comma-separated list of quirks to be applied to a game. The LD_LIBRARY_PATH runtime checks for search-cwd and/or search-cwd-first, and SLR doesn't currently need to pay any attention to that variable (but it could, if that would be helpful).

If Steam ends up using a separate depot to distribute a shared copy of Noto, then we would probably want to make that a separate environment variable in the STEAM_COMPAT_ namespace, to avoid having to define an escaping/unescaping mechanism for paths that happen to contain commas.

flibitijibibo commented 1 year ago

Did a bit of digging into the Unity docs (since, again, I'm 100% sure we're telling the editor to bake the font files into the assets), and came across this:

If none of the listed fallback fonts are present and have the requested glyph, Unity will fall back to a hard-coded global list of fallback fonts, which contains various international fonts commonly installed on the current runtime platform.

So it's very possible that we could just dump the string list from UnityPlayer.so and incorporate those into a "Unity Fallback Fonts" appid or something.

In the meantime, I'm in the process of possibly fixing this for Superliminal, here's what I'm working on in case someone finds this thread via Google:


From an internal e-mail with Pillow Castle:

I came across a particular line in the Unity font spec:

… or the font does not include the requested glyph (like when trying to render text in east Asian scripts using a latin font, or when using styled bold/italic text), then it will try each of the fonts listed in the Font Names field, to see if it can find a font matching the font name in the project (with font data included) or installed on the user machine which has the requested glyph.

I checked the fonts and it seems like it does have some font names, but maybe the best fix is to just shotgun-blast all the fonts on the system to reference fonts we know we’re embedding?

To explain as best as I can, because I barely understand this myself: On flibitDemon all the fonts work except for Arabic, but if I go to Superliminal_FONTS/NotoSans-ExtraCondensed.ttf, go to the “Font Names” textbox, and change “Noto Sans” to “Noto Sans Arabic UI”, it fixes the problem for me. So maybe we just need to go through all the fonts and add all the possible names to each one, to be absolutely sure it uses the embedded ones instead of digging through the OS.

smcv commented 1 year ago

I'm not really clear on how choosing script-specific font subsets is meant to work myself: half the time a font like Noto Sans behaves like a single monolithic font named Noto Sans (which, as an implementation detail, is spread between multiple files), and the other half it behaves like a group of related fonts with names like Noto Sans Arabic UI which are individually incomplete, but add up to provide complete coverage between them.

flibitijibibo commented 1 year ago

Turns out the fix was in fact that shotgun blast idea, which means it's extremely likely other Unity games have this too... :psyduck:

We haven't pushed it to the public branch yet, so feel free to keep using this game as a test case (and if you need a key, just e-mail me). Once the public version is updated we'll put the current one in a passworded branch for the team to keep testing against, if needed!

flibitijibibo commented 1 year ago

Branch is live, password is languageplease.

mdiluz commented 1 year ago

Assuming this hasn't been reported through our normal support channels already - I pinged this over to my Linux colleagues at Unity and they're happy to investigate getting this resolved for future revisions, but we're wondering if a small reproduction project exists that could be shared? Otherwise it'll be hard to properly validate a fix. If Pillow Castle already have a support relationship with Unity then it may be best to share it through those channels to avoid an IP leak.

flibitijibibo commented 1 year ago

Hit this with Marble It Up Ultra which is releasing on August 17th - I don't suppose we ever came across a way to deduplicate these font archives?

TTimo commented 1 year ago

@smcv would we be able to add the subset of needed fonts in sniper to address this? My understanding is that latest or a future update of unity might resolve this, but it doesn't help folks on older versions.

TTimo commented 1 year ago

Oh I see you suggested a steam based compat flag solution above. Ok we'll have to see if we can prioritize work on that.

FriedelVerpoort commented 10 months ago

@TTimo we're having the same issue on art of rally. I was hoping to fix this with the suggestion from @flibitijibibo however after shotgun-blasting all our fonts inside the "Font Names" textbox and making a build we're still having the issue. Maybe it's because I'm misunderstanding what the exact steps were but our all our fonts Font Names field contain this font list now:

Noto Sans
Noto Sans KR
Noto Sans JP
Noto Sans SC
...

Should only the faulty fonts be added? Or is there another step that I skipped over. I'm asking here because I don't have a Steam Deck myself which makes testing fixes a little bit more cumbersome.

However I was also wondering if the steam based compat flag solution has seen any developments?

TTimo commented 10 months ago

This is unfortunate - ideally this should be addressed by Unity, in the meantime developers may have to use the workaround published by @flibitijibibo.

Our current resources and priorities have not allowed us to look for a workaround in the Steam for Linux runtime environment.

flibitijibibo commented 10 months ago

The workaround is two parts, one for the .asset and the other for the font meta files.

The .asset changes look something like this...

-  m_FallbackFontAssetTable: []
+  m_FallbackFontAssetTable:
+  - {fileID: 11400000, guid: 350bf0deb6ebd49b6bb5a8253129b1ac, type: 2}
+  - {fileID: 11400000, guid: d63c98e7259d63a4fb7ee9650908e0d2, type: 2}
+  - {fileID: 11400000, guid: 37a011faa5f946e4db85c9264a10f581, type: 2}
+  - {fileID: 11400000, guid: b679a8c874231d7449b6611015b65122, type: 2}

... while the .meta changes look like this:

-  fontName: Noto Sans Arabic
  fontNames:
  - Noto Sans Arabic
-  fallbackFontReferences: []
+  - Noto Sans CJK JP
+  - Noto Sans
+  fallbackFontReferences:
+  - {fileID: 12800000, guid: c193af093c404490f95fce78312b2675, type: 3}
+  - {fileID: 12800000, guid: edbb569d50b0543fb823db5a345b38d5, type: 3}
+  - {fileID: 12800000, guid: 558c0f8baa2770a4d872e4196fa28ca6, type: 3}

It's a combination of adding every font to every asset, and then adding every other font to every font. Be careful though, the order of the font list can matter; if for example you put CJK at the top and the character is actually in the Arabic font, it seems to search the entirety of those first fonts before digging up the correct one, so this workaround has performance implications that the correct fix would avoid.

nisehime commented 8 months ago

Even if it's a Unity's fault, this issue is more likely to occur on Steam Deck because Valve is forcing container runtime on it for these games, which is not the case with other Linux distros. And users cannot change it without external tools.

This is a general problem with current Deck verification process that they don't check the operation in languages other than English. There are several titles with green mark that do not work or not work properly when played in non-English (mostly CJK) languages.