Search Unity

Bug targetFrameRate not working in 2021.3.1 LTS WebGL

Discussion in 'Web' started by ammars26, May 13, 2022.

  1. ammars26

    ammars26

    Joined:
    Nov 7, 2016
    Posts:
    13
    I'm working on a game for WebGL platform, I was earlier working before on Unity 2020 LTS. I fixed the FPS to 60 with the help of Application.targetFrameRate and it was working, but as soon I upgraded my project to 2021 LTS. It stopped working, it only works in editor but on WebGL platform inside browser it adapts to the current refresh rate of monitor.

    If i downgrade my project back to 2020 LTS, it works. But if I upgrade it to 2021 LTS it stops working. Nothing changed in project. Is it something I'm missing in 2021 regarding WebGL?

    I have also tried setting vsync to 0 via code. It didn't resolved anything. Its only happening to 2021.3.1 LTS

    Update: When i turn vsync off and set it to 0 and set targetFrameRate any value except 60 (even 50 or 61) it works. Its only not working on value of 60
     
    Last edited: May 14, 2022
    xucian likes this.
  2. brendanduncan_u3d

    brendanduncan_u3d

    Unity Technologies

    Joined:
    Jul 30, 2019
    Posts:
    437
    If you're on a high refresh monitor, like a new Macbook Pro has a 120hz monitor, then the logic we have for targetFrameRate will fail for 60 because we have a hard-coded assumption that the browser is running at 60hz. It tries to be smart and if you set it to 60, it uses the native refresh rate instead. It was only recently that Chrome added support for the higher refresh rates, and we'll need to rethink the logic we have for that.

    That said, setting targetRefreshRate will always be less efficient than allowing it to use the native refresh rate, since it would use a timer instead of the browsers native update loop.
     
  3. ammars26

    ammars26

    Joined:
    Nov 7, 2016
    Posts:
    13
    Thank you for the detailed response. But what if we really want to force the target frame to 60 on every type of display (120Hz or even 144Hz). You can consider this a very strict hard requirement for us, in any case we want our it to run at 60 at max. Is there any other way?

    Because our app on WebGL must run at 60 FPS since its embedded inside a webpage that tries to record and sync with it at 60 FPS, it's essential to our requirement.

    Currently we are working on Unity 2020 with this method, but we would like to upgrade to 2021 version. Please let us know if there is anyway for us to do that.
     
  4. OceanX000

    OceanX000

    Joined:
    Feb 24, 2021
    Posts:
    120
    But, when set targetFrameRate to 15, fps remains 60hz. This didn't happen until Unity2021.
    1. Set targetFrameRate
    upload_2022-6-1_11-23-31.png

    2. Print function called:
    function _emscripten_set_main_loop_timing(mode, value) {
    Browser.mainLoop.timingMode = mode;
    Browser.mainLoop.timingValue = value;
    console.log('_emscripten_set_main_loop_timing', mode, value);

    3. Game's FPS remains 60hz
    upload_2022-6-1_11-24-51.png
     
  5. ammars26

    ammars26

    Joined:
    Nov 7, 2016
    Posts:
    13
    Try setting it to some odd number like 14 or 29 or 59 and it will reflect. Its only happening in Unity 2021. Also make sure you set vsync to 0
     
  6. brendanduncan_u3d

    brendanduncan_u3d

    Unity Technologies

    Joined:
    Jul 30, 2019
    Posts:
    437
    @OceanX000 Looking at the code that controls emscripten_set_main_loop_timing:

    * Make sure to set the vsync to 0, otherwise it will override the specified targetFrameRate to 60 / targetVSync.

    * If targetFrameRate % 60 == 0, then it will always use EM_TIMING_RAF instead of EM_TIMING_SETTIMEOUT, and the skipFrames will be 60 / targetFrameRate.

    So, if targetFrameRate is 15, it should be calling emscripten_set_main_loop_timing(EM_TIMING_RAF, 4).

    Unfortunately there is no way in the browser to reliably query or measure the framerate of the monitor, which can also change if you move the window from one monitor to another. This means targetFrameRate of 0 will always use (EM_TIMING_RAF, 0) and play every frame (or as fast as it can), which on Chrome on a 144Hz monitor, will play 144fps (assuming the game can keep up). We've talked with Google about this but there is no web spec to query this.

    You could always hijack emscripten_set_main_loop_timing like you did to print out the values, and replace the Unity specified values with your own, _emscripten_set_main_loop_timing(0, 1000/60). Note that using a timeout mainloop will likely cause inconsistent and possibly unsmooth playback, because requestAnimationFrame was designed to be used for rendering. Also, it's possible some browsers, Firefox possibly, might not render correctly if not rendered from a requestAnimationFrame callback.
     
  7. ammars26

    ammars26

    Joined:
    Nov 7, 2016
    Posts:
    13
    @brendanduncan_u3d sorry i'm a bit confused for targetFrameRate % 60 == 0 (when targetFrameRate is 15 then it should be false, then how its still using EM_TIMING_RAF)

    Also how we can force EM_TIMING_SETTIMEOUT when targetFrameRate is 60

    Another problem is we are trying to achieve it closer by setting it to 59 but it causing our frames to freeze for no reason

    This is only happening in Unity 2021 (old versions are okay, we would like to achieve it on new version if possible)
     
  8. brendanduncan_u3d

    brendanduncan_u3d

    Unity Technologies

    Joined:
    Jul 30, 2019
    Posts:
    437
    I'm sorry, I meant 60 % targetFrameRate, so 60%15 == 0 and it does use RAF.
    I don't know why that would cause your frames to freeze, though in general it's highly un-recommended to use timeout for rendering. Looking at 2020, it's the same logic for RAF vs timeout. If the freezing just happens in 2021+, then maybe the newer Emscripten is doing some funny busines with timeout main loops. I'd have to have a repo to test it.

    The whole framerate business with requestAnimationFrame is a mess because Google and Apple can't come to an agreement on how it should work, which makes it a fragmented mess. We haven't found a robust way to reliably solve the problem yet, though wanting to limit framerates is more of an edge case (obviously one important to you).
     
  9. robrab2000-aa

    robrab2000-aa

    Joined:
    Feb 8, 2019
    Posts:
    117
    Thanks, this fixed my issue! I was wondering, here and in the docs its recommended to let the browser dictate the frame rate. When we run our build the fans on our machine start going crazy and the gpu usage is really high (this is across multiple machines such as M1 Max 24 gpu core and windows machines with 20180ti, 3080 mobile, 3090 desktop and 1080 desktop). What's shown on the screen seldom changes as most of the time a user is just using it to view static content. So we really don't need it to be updated every frame, only when being interacted with.

    Will limiting the framerate with targetFrameRate to something low really be less efficient than running it at max speed?
     
  10. tinyant

    tinyant

    Joined:
    Aug 28, 2015
    Posts:
    127
    Unity2021.3.16LTS
    mobile webgl


    Need 60 % targetFrameRate != 0

    targetFrameRate = 31

    QualitySettings.vSyncCount = 0;
    Application.targetFrameRate =targetFrameRate;

    it works.
     
  11. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    I am also very interested in what robrab2000-aa asked: is setting the target frame rate to -1 (i.e. letting the browser decide) really the "better" way when the goal is to be less resource-hungry?
    I'm sure I used to be able to limit the frame rate to 30 to not have laptops put their fans into overdrive (or the M1 MacBooks actually produce fan noise), but now I can't seem to influence the actual frame rate at all, even when using 31 instead of 30 as the target number and setting vSyncCount to zero.
    Is there a reliable way to cap the frame rate in WebGL nowadays? (@brendanduncan_u3d ?)
     
  12. brendanduncan_u3d

    brendanduncan_u3d

    Unity Technologies

    Joined:
    Jul 30, 2019
    Posts:
    437
    @uwdlg Letting the browser decide on the refresh rate (reqeustAnimationFrame) is the better way from the browsers perspective, in terms of trying to maintain a consistent framerate. Alternatively, skipping frames from requestAnimationFrame is also a reasonable solution. Because there is no way to reliably query the browser for the refresh rate requestAnimationFrame is targetting, which is dependent on the refresh rate of the current monitor, Unity makes the arguably naïve assumption of 60 Hz, because that's still the most common monitor refresh rate. Unity will use requestAnimationFrame and skip frames if your target frame rate is evenly divisible from 60. For example, a target frame rate of 30 will use requestAnimationFrame and skip every other frame. A target frame rate of 15 will use requestAnimationFrame and render every 4th frame. Setting the target frame rate to any other value will use a timer instead of requestAnimationFrame for the rendering rate. While this gives you some more control, there is no way for the browser to guarantee to any reasonable degree that timing, because the browser will still composite the page at the rate it wants to, and the frame you just rendered may have to wait for that to be displayed. VSync options in Unity have no affect for the web platform because there is no way to know what the vsync rate is or wait for it from a browser. If setting the target frame rate is not working, as you seem to be saying, then that is a Unity bug.
     
  13. robrab2000-aa

    robrab2000-aa

    Joined:
    Feb 8, 2019
    Posts:
    117
    Right, thanks. so does this mean that I can get it to only render every 4th frame by setting target frame rate 15? Ideally I'd like to have it updating very infrequently most of the time and then occasionally (on user input) have it update at 60 or whatever the browser wants?
     
  14. brendanduncan_u3d

    brendanduncan_u3d

    Unity Technologies

    Joined:
    Jul 30, 2019
    Posts:
    437
    It might be possible to change the target frame rate at runtime, switching between say 15 and -1. Perhaps change it to -1 if you get some input, and use a timer to change it back to 15 after some period if no input. But there would be no way to automatically do this, and I've never tested this to know if it would work or if I'm just making things up.
     
  15. robrab2000-aa

    robrab2000-aa

    Joined:
    Feb 8, 2019
    Posts:
    117
    great, thanks I'll give it a go and let you know how I get on :) The main thing is that we have super heavy SSAO (which is essential for our use case) so even if I could get the post processing to not update every frame that would be amazing
     
  16. brendanduncan_u3d

    brendanduncan_u3d

    Unity Technologies

    Joined:
    Jul 30, 2019
    Posts:
    437
    You can't update a screen space effect independently of rendering the scene, since the effect is dependent on what you rendered. So, unless turning off the effect for certain frames is an option, the only other option is to not render the frames as much as you can get away with, and target frame rate is the only real tool you have to control that.
     
    robrab2000-aa likes this.
  17. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    Thanks for the reply!
    It does look like a bug to me:
    I just made a new empty project with the same Unity version I first noticed this with (2021.3.18f1), adding a script to a GameObject in the scene setting
    Application.targetFrameRate = 30;
    in Start(), disabling build compression and Auto Graphics API and made a build. I also repeated the same process for a second new project in the currently latest 2022 LTS 3.5f1 version.
    I checked both builds hosted on a GitLab Pages page on Windows 10/11 in Brave and Edge, using the Developer Tools' "Frame Rendering Stats" overlay from the Console drawer's Rendering tab, and I get significantly higher fps than the expected ~30, seemingly still trying to match the screen refresh rate (e.g. 230+ frames per second on a laptop with a 240 Hz screen).
    I reported a bug with the 2022 project attached (Case IN-49893) and will link to it once it goes public.
     
  18. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    I just checked another build I've had online for close to a year where that method of capping the frame rate definitely worked in the past, so I'm wondering if some browser update broke that?
     
  19. brendanduncan_u3d

    brendanduncan_u3d

    Unity Technologies

    Joined:
    Jul 30, 2019
    Posts:
    437
    I'll holler at people to look into it and get it fixed.
     
    MousePods, robrab2000-aa and uwdlg like this.
  20. unityruba

    unityruba

    Unity Technologies

    Joined:
    Nov 6, 2020
    Posts:
    273
    as always I'm going to hop on here and say: opening a bug report with a repro project would make fixing this a 1000 times faster :D
     
    bugfinders likes this.
  21. unityruba

    unityruba

    Unity Technologies

    Joined:
    Nov 6, 2020
    Posts:
    273
    welp, there was a bug report in the thread! I missed it! and also I've been hollered at so I'll keep you all updated!
     
    LilGames, AndreaGalet and KamilCSPS like this.
  22. AndreaGalet

    AndreaGalet

    Joined:
    May 21, 2020
    Posts:
    101
    I'm not finding it in the issue tracker and here in the thread, can you post it here?
     
  23. KamilCSPS

    KamilCSPS

    Joined:
    May 21, 2020
    Posts:
    448
     
  24. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    The bug report status was changed to "Under Review" 7 hours ago, still no link I can share though.
     
    AndreaGalet likes this.
  25. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    bugfinders likes this.
  26. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    Thanks for the bug report. The root cause of the issue that Application.targetFrameRate = 30; is being ignored is that according to the documentation, the setting QualitySettings.vSyncCount is intended to take precedence over Application.targetFrameRate if vSyncCount is set.

    That is, in https://docs.unity3d.com/ScriptReference/QualitySettings-vSyncCount.html it is stated

    "Unity ignores the value of targetFrameRate if you set vSyncCount."

    A more correct phrasing of the current implementation on Web platform is that

    "If QualitySettings.vSyncCount has a nonzero value, then Unity ignores the value of Application.targetFrameRate."

    To quickly work around this issue, you can set

    QualitySettings.vSyncCount = 0;
    Application.targetFrameRate = 30;

    and after that, it should give a frame decimation factor of two. Alternatively, setting

    QualitySettings.vSyncCount = 2;
    Application.targetFrameRate = 0;

    should have the same effect (although didn't test that one).

    I agree the above mechanism is mighty confusing. We need to clarify this more, and improve the documentation.

    The reason that Application.targetFrameRate is currently behaving oddly with respect to setting a value that divides into 60 evenly, vs setting another value like 59 or 29 is a fallout from this long-standing web browser platform limitation: https://github.com/whatwg/html/issues/8031

    But unfortunately the issue has not progressed, so there is currently no great solution overall for developers who wish to multi-target 60hz, 90hz, 120hz etc. displays using a non-native refresh rate. We have some plans to do bandaids by benchmarking refresh rates to see if that might be "good enough"(tm), but without a proper web API added to browsers, it will unfortunately not be perfect.
     
  27. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    Something to note here is that Chrome's Frame Rendering Stats window cannot be used to correctly detect if frame decimation is taking place. When Unity skips rendering on every second vsync count, Chrome's profiling tool does not understand that, and it simply reports every vsync as being rendered on.

    To accurately measure if frame decimation is taking place, edit the ProjectSettings\ProjectSettings.asset file, and in that file, find line "webGLEmscriptenArgs:" and add the following argument to it:

    Code (csharp):
    1. webGLEmscriptenArgs: --cpuprofiler
    Then do a build. That will embed an inline performance profiler view, which should show jagged/combed lines of blue and green spiking up from the profiler, as follows.

    upload_2023-8-29_12-5-4.png

    In this screenshot, my display has a native refresh rate of 165 Hz, and with a decimation factor of two, the effective rendering rate is 82.5 fps, but Chrome's Frame Rate will still show around ~165 Hz, just every second frame went unrendered.
     
  28. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    Thanks @jukka_j for the detailed answers, the Github issue is also very informative (plus the exchange here where I think you also answered and that offers more context for the rest of this post).

    On a side note, in addition to the note about the Frame Rendering Stats panel, I also realized I got served cached versions for some of my tests despite clearing the site data (which I wrongfully assumed would suffice) further distorting my observations, sorry.

    I see the bug is being tackled by refining the documentation for now, as long as that underlying beast of an issue still persists.

    In the meantime I've tried my hand at adjusting the frame rate dynamically with a simple/naive benchmarking method (for now I just check if Time.deltaTime is below a certain value for a number of consecutive frames and try to further reduce the frame rate if so by increasing the vSyncCount). That works reasonably well (putting of dealing with varying display refresh rates as mentioned in the Github issue for now), but for very high refresh rates (e.g. my 240 Hz display), I have a bit of a problem:
    As per the documentation, QualitySettings.vSyncCount is limited to a value of 0, 1, 2, 3 or 4. Thus I can get down to 60 fps by setting vSyncCount to 4 (because 240 Hz / 4 == 60), but can't get to the 30 fps I want (for which I would need vSyncCount to be 8). I think the other way via Application.targetFrameRate also wouldn't really work, since I would need to set 60 / 8 == 7.5, plus I would like to stick to requestAnimationFrame and decimation. I also tried to use the OnDemandRendering API, but that doesn't seem to work with WebGL at all.

    So I think the best way is to directly call the framework function that Unity normally calls when changing vSyncCount / targetFrameRate, which should be _emscripten_set_main_loop_timing(mode, value) from <buildname>.framework.js and for the 240 Hz example, I think I'd need to call it with mode=1 and value=8 to have it use requestAnimationFrame but skip rendering for all but the every 8th call.

    So the question is: how can I call _emscripten_set_main_loop_timing from the Unity code (I know how to make JavaScript calls from C#, but how can I get to that function via - presumably - a unityInstance reference)?
     
  29. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    Okay, this is how I got it working:

    I added a .jspre file to /Plugins/WebGL/ with
    Module["_emscripten_set_main_loop_timing"] = _emscripten_set_main_loop_timing;
    in it.
    That line gets included into the top of the <build_name>.framework.js(.br) file and allows access to the _emscripten_set_main_loop_timing via
    Module
    , which is in the same namespace as JavaScript functions defined in Unity via .jslib (as documented here, sadly without the .jspre part).

    My .jslib file looks like this:
    Code (JavaScript):
    1. mergeInto(LibraryManager.library, {
    2.     AdjustDecimationCount: function (decimationCount) {
    3.         if (Module && Module._emscripten_set_main_loop_timing) {
    4.             Module._emscripten_set_main_loop_timing(1, decimationCount);
    5.             console.log("set decimation value to " + decimationCount);
    6.         } else {
    7.             console.log("Error: Unity build couldn't find _emscripten_set_main_loop_timing function!");
    8.         }
    9.     },
    10. });
    Finally, I added the following C# script to my test scene:
    Code (CSharp):
    1. public class FrameRateAdjuster : MonoBehaviour
    2. {
    3.     private int _consecutiveOvers;
    4.     private int _consecutiveUnders;
    5.  
    6.     [DllImport("__Internal")]
    7.     public static extern void AdjustDecimationCount(int decimationCount);
    8.  
    9.     private void Update()
    10.     {
    11.         if (Time.deltaTime < 1 / 40f)
    12.         {
    13.             _consecutiveOvers += 1;
    14.             _consecutiveUnders = 0;
    15.             if (_consecutiveOvers == 30)
    16.             {
    17.                 ChangeDecimationCount(+1); // increase number of frames in which rendering is skipped
    18.                 _consecutiveOvers = 0;
    19.             }
    20.         }
    21.         else if (Time.deltaTime > 1 / 25f)
    22.         {
    23.             _consecutiveUnders += 1;
    24.             _consecutiveOvers = 0;
    25.             if (_consecutiveUnders == 30)
    26.             {
    27.                 ChangeDecimationCount(-1); // decrease number of frames in which rendering is skipped
    28.                 _consecutiveUnders = 0;
    29.             }
    30.         }
    31.         else
    32.         {
    33.             _consecutiveOvers = 0; // Mathf.Max(0, _consecutiveOvers - 1);
    34.             _consecutiveUnders = 0; // Mathf.Max(0, _consecutiveUnders - 1);
    35.         }
    36.     }
    37.  
    38.     private void ChangeDecimationCount(int change)
    39.     {
    40.         _decimationCount = Mathf.Max(0, _decimationCount + change);
    41.         AdjustDecimationCount(_decimationCount); // calls JavaScript function from .jslib
    42.     }
    43. }
    So I keep track of frames in which Time.deltaTime is above or below some magic thresholds (around 1/30 because I want to enforce 30 fps) and if either happens for 30 frames in a row (a naive way to try and ignore short frame rate drops / spikes), I increase or decrease the "decimation value" n, meaning rendering is skipped for all but every nth frame (this is implemented in _emscripten_set_main_loop_timing which takes this n as second parameter, while the first determines the mode of render loop timing, where 1 means use requestAnimationFrame and perform decimation as I described).
    This is by no means a great or finished implementation, but it seems to work for me so far. Both the threshold values for when the number of frames skipped should be increased or decreased and the magic 30 consecutive frames may need tuning. At least as it is now, it should adapt the frame rate if the base vsync rate decides to change.
     
    Last edited: Nov 18, 2023
    yty likes this.