Search Unity

Disabling V Sync on Android

Discussion in '2D' started by Jakems, Apr 1, 2019.

  1. Jakems

    Jakems

    Joined:
    Nov 23, 2017
    Posts:
    6
    Hi, I was wondering if it is possible to disable Vsync for Android Devices.
    I allready unchecked the Vsync Checkbox in the Quality settings for the "Very Low" Graphic Setting,
    which is the Default one for Android Builds.
    When running the Game in the Editor the Profiler shows me, that no CPU usage goes to the Vsync
    calculation, but when I Build the Game on my Android device, Vsync takes so long to Calculate, that
    my game lags.
     
  2. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    Hi Jakob,

    While the profiler shows V Sync the same way s it does other calculations, it is basically idle time where nothing is calculated. It just waits until the next appropriate V Blank comes along to flip the frame over.

    Here is some further explanation on commonly confusing Samples from an upcoming update to the documentation:
    • WaitForTargetFPS: Time spent waiting for the targeted FPS specified by Application.targetFrameRate. The Editor doesn’t VSync on the GPU and instead also uses WaitForTargetFPS to simulate the delay for VSync.

    • Gfx.ProcessCommands: This sample on the render thread encompasses all processing of the Rendering commands on the render thread. Some of that time might be spend waiting for VSync or new commands from the main thread, which can be seen from it’s child sample Gfx.WaitForPresent.

    • Gfx.WaitForCommands: This sample indicates that the render thread is ready for new commands and might indicate a bottle neck on the main thread.

    • Gfx.PresentFrame: This render thread sample is time spent waiting for the GPU to render and present the frame, which might include waiting for VSync.
    • Gfx.WaitForPresent: When the main thread is ready to start rendering the next frame, but the render thread has not finished waiting on the GPU to Present the frame. This might indicate that your game is GPU bound. Look at the Timeline view to see if the render thread is simultaneously spending time in Gfx.PresentFrame. If the render thread is still spending time in Camera.Render, your game is CPU bound and e.g. spending too much time sending draw calls/textures to the GPU.
    So to get to the bottom of this, you'll need to profile the game on your target device (which is always advisable, as the Editor functions fundamentally different, even to a Standalone build). Since this is an issue around the interplay of the Main thread, the Render thread and the GPU, you'll want to look at the Timeline view of the CPU Usage Profiler module instead of the Hierarchy view, which only shows the Main thread.

    One thing that happens on mobile devices is that Application.targetFrameRate is by default set to -1, from the documentation of that field:
    "- On mobile platforms the default frame rate is less than the maximum achievable frame rate due to need to conserve battery power. Typically on mobile platforms the default frame rate is 30 frames per second."

    so you might want to set that to 60, but keep in mind that you're phone will then use more power, heat up faster and as it heats up, the device might get throttled by the OS.

    Additionally I'd check if you get any changes if the VSync setting for all quality levels is set to not vsync. If you want to only affect mobile devices to not VSync you can also add this code to e.g. some startup logic of the game

    Code (CSharp):
    1. if(Application.isMobilePlatform)
    2.     QualitySettings.vSyncCount = 0
     
  3. Pacosmico

    Pacosmico

    Joined:
    Jan 16, 2014
    Posts:
    14
    Hello, I just want to say,
    I have been screaming of pain because I had a massive VSync Time (30 fps cap) with the Quality Setting . VSync set to -Don't Sync- and turns out Don't Sync actually means Sync Every2 (30fps)????? WTF???
    I just tested it, Don't Sync == Every Second V Blank (same effect)
    Every V Blank == NO CAP (250 fps according to Profiler connected to actual Android device)

    I'm using 2019.1.8 (not sure if this was always like that)

    This is very non intuitive and really really very annoying indeed!!. Sorry for the tone.

    Saludos
     
    oneted_7, JBR-games and chrismarch like this.
  4. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    Please Check this behavior against what the Documentation for targetFrameRate says. That the documentation for VSync does not alert to this behavior is a separate issue that I've noted down to be addressed in documentation.

    If the behavior is other than documented, please file a bug and post the issue ID here.
     
    Pacosmico likes this.
  5. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    To be clear, I'm not saying it's fine that what you described should be expected behavior, just that VSync settings and targetFrameRate have interleafing effects on each other and you did not describe if you also change them/ what their status was.
     
    Pacosmico likes this.
  6. Pacosmico

    Pacosmico

    Joined:
    Jan 16, 2014
    Posts:
    14
    There is not a single line in the project that touches "Application.targetFrameRate". That member is not referenced in the code at all, this is just a Quality setting change.
    According to my teammates, it's unclear when or why the Quality configuration was changed (that's on Us probably, no problem) but when trying to fix the low fps (30) issue, we checked the VSync setting to make sure it was not set to "Every Second Blank", and it was not (it was on Don't Sync) and we did not associated that setting with the documentation of the "targetFrameRate", it might be useful to link that in the Manual Page of the Quality Settings
    What also intrigues me, is that, the setting is currently "Every Blank" and yet the app runs at 250, as if no cap is being applied (I would expect it to run at 60?)
    this is all on device (not unity editor)

    thanks for answering so fast, btw

    DOCS (targetFrameRate)>
    Additionally if the QualitySettings.vSyncCount property is set, the targetFrameRate will be ignored and instead the game will use the vSyncCount and the platform's default render rate to determine the target frame rate. For example, if the platform's default render rate is 60 frames per second and vSyncCount is set to 2, the game will target 30 frames per second.
    (Note: neither this setting is modified or even referenced in the project code)
    I think the problem is that "VSync = Don't Sync" reads kinda like "Dont drop frames" and actually is more like "Device Default"
    The other thing that still I don't quite understand is why setting Every Blank does not cap to 60. I mean, maybe it's just that device refresh rate is actually 250, but then Every Second VBlank, shouldnt drop to 30. I also can see how I might be misunderstanding some fundamentals here.

    sorry for the eclectic post, I was writing and reading your pointers at the same time

    So, to wrap up, I think the documentation is not wrong, or the behaviour. It is that Quality Settings "Dont Sync" means, use "platform default target framerate", and that is only pointed out in the documentation for target framerate (which is not linked to in the Quality Settings Manual Page) and I think that may be misleading?
    Like this, targetFrameRate default = -1, which means default for the platform. Android default = 30fps
    Quality Settings Default = Every Blank (which overrides "targetFrameRate" according to docs, hiding this behaviour from start) meaning fps = device RefreshRate (very high this days?)
    so setting Quality Settings to Don't Sync, makes "targetFrameRate" to NOT being overrided, and there is where it kicks in (by default set to -1, which in turns means default for platform, which for Android means 30fps which is the same as vSync = 2)

    That's my summary, I hope it's not too confusing
    Greetings
     
    oneted_7 and chrismarch like this.
  7. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    Hey again,

    yes, I think one part of the confusion is, that there is VSync, and then there is capping the framerate due to target OS defaults and restrictions, that might look like VSync but aren't, though also behave the same on some devices... it could definitely be highlighted.

    Regarding the 250 FPS on Android, I think regardless of what settings you choose, reaching 250 FPS should not happen. That's just going to needlessly burn the battery and heat up the phone, which is why there is a 30 FPS cap by default and an enforced 60 FPS in the first place. So for that, it would be super nice if you could create a bug report for that, also stating what android device this happened with. The mobile team seemed unaware of any such issues.

    P.S. running at 250 FPS is rather inadvisable, not only are your players going to complain about the battery drain and burning their fingers on the phone. a hot phone will also go into throttled mode at some point to keep it from overheating entirely. And at that stage, performance is going to drop. Not sure if that makes a difference when running at 250 FPS but still ;)
     
    filcomet, Helladah and Pacosmico like this.
  8. Pacosmico

    Pacosmico

    Joined:
    Jan 16, 2014
    Posts:
    14
    Hi, sorry for not responding for a while,
    I just made a very simple project to just be sure I understood everything correctly. In the meanwhile I discovered that the profiler (via adb) seems to not show always the VSync cost of the app (hence why I saw 250fps on profiler) but I added a fps counter on the app and as expected, it never goes over 60fps

    Gracias por la ayuda, un abrazo!
     
    MartinTilo likes this.
  9. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    That's problematic. In what way does it not show it?
    If you look in the CPU Profiler's Timeline View, does it show WaitingForTargetFPS? Is there a WaitingForSemaphore sample underneath? Does it not show VSync in the CPU Profiler's Frame Chart? If it's that, it's likely due to this bug. If not, I'd appreciate a bug report so we can fix it :)
     
  10. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    Vsync & Mobile, here a my findings:

    This is how you should set Vsync:
    Code (CSharp):
    1. public void SetVsync1            ()    {    QualitySettings.vSyncCount = 1;    Application.targetFrameRate =  -1;    }
    2. public void SetVsync2            ()    {    QualitySettings.vSyncCount = 2;    Application.targetFrameRate =  -1;    }
    3. public void SetVsync0_Default    ()    {    QualitySettings.vSyncCount = 0;    Application.targetFrameRate =  -1;    }
    4. public void SetVsync0_60FPS        ()    {    QualitySettings.vSyncCount = 0;    Application.targetFrameRate =  60;    }
    5. public void SetVsync0_120FPS    ()    {    QualitySettings.vSyncCount = 0;    Application.targetFrameRate = 120;    }
    6.  


    This is how you should use above to get the best fluidity:
    SetVsync1 - Use ONLY when your game can maintain 60 fps. If not, use SetVsync0_60FPS or SetVsync0_120FPS, because your game will drop less frames then.
    SetVsync2 - Use if you barely get 40 fps. It feels definitely better to have 30 consistent evenly spaced frames than 40 frames with varying delays between them.
    SetVsync0_Default - Does whatever the platform does
    SetVsync0_60FPS - Try this if you don't reach 60fps, it will look better than SetVsync1
    SetVsync0_120FPS - Try this if you don't reach 60fps, it might look even better than SetVsync0_60FPS. It looks like less frames are dropped, i have no explanation, just try it yourself.
    (Tested on Samsung phones, A3 2017, S6 edge+)

    Theory:
    I believe Android has a mandatory vsync phase. I believe this because i never see screen-tearing regardless of vsync and frame rate. This could explain why SetVsync0_60FPS & -120FPS look better, because then the device might be able to catch more of these mandatory syncs.
    (source: https://support.unity3d.com/hc/en-u...game-flips-between-running-at-30FPS-and-60FPS)



    Other Hints:
    Do your own framerate measurements (i attached my currently used script)
    The editor always shows maximum possible framerate to give performance hints, here you see editor vs measured frames:
    upload_2019-12-19_12-31-58.png
    Also, see here: https://forum.unity.com/threads/turn-on-vsync-in-the-editor-scene-window.276720/#post-5282394
     

    Attached Files:

    Last edited: Dec 19, 2019
    WaqasGameDev likes this.
  11. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    Don't attempt to get accurate performance measurements, especially not the frame rate, while running in the Editor. The Game View and PlayerLoop are running on the same Main Thread as the Editor. What the Stats View does is it calculates the framerate based on the PlayerLoop time, ignoring (or rather calculating out) that the Main Thread spends both time on PlayerLoop as well as updating and drawing the Editor surrounding the game view. While this is more accurately representing what would happen in a built player that is entirely main thread bound, it completely falls apart for anything that is Render Thread or GPU bound or spends significant time in other threads that then need to be synced on in a different segment of one of the two PlayerLoop sections of the frame, than the one it was spawned from, because the timing is going to be off.

    There is no way to calculate around these thread/GPU timings being different within the Editor, not even with your own script. In fact, your own script will likely just go by the time that passed between the frames so you'll measure the performance of the Editor along side your game. Probably not what you intended ;)
    Check out the Profiler's CPU timeline view to get a better understanding of these timing issues.

    Also I'd be wary about the results of setting a targetFramerate higher than 60. Since phones sync to the 60 Hz frequency anyways, I'd have assumed it would ignore such a setting, possibly clamping it or falling back to the default -1. Haven't checked that yet though
     
    TigerHix likes this.
  12. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    True, i primarily meant runtime measurement on the target device

    I would assume the same. But when my device goes down to 50fps, I absolutely see a differences between my SetVsync1, SetVsync0_60FPS and SetVsync0_120FPS function.

    • I understand that SetVsync1 looks worse than the others because of the dropped frames. I can even measure the lowered frame rate due to frame dropping (SetVsync1:40fps vs SetVsync0_60|120FPS:45 fps).
    • But i don't understand how there can be a visible difference between targetFrameRate = 120 and targetFrameRate = 60 for me. The reported framerate stays the same (~45), yet i can tell the visuals are much smoother (rotating the camera)
    But don't trust my perception, test it yourself. Create a scene with black and white objects and controllable complexity (e.g. object count). Then you can artificially limit the framerate by render-complexity. Then try the different Set functions and move stuff.

    Or should i try to film my phone screen frame by frame?
    EDIT: I did:
     
    Last edited: Dec 19, 2019
  13. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    @Marrt It's a bit of a mystery why that would make a difference, even when looking at the code. In what update loop are you updating the position of the ship and your camera? maybe the delta time is somehow affected by this in a way I couldn't see straight away from the code.

    Also, have you had a look at the Optimized Frame Pacing option for Android? That might also help to smooth things out.
     
  14. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    <arrogance> I use an pedantic Tickmanager to sequence everything. Because the Unity System of Update,FixedUpdate etc. is severly lacking in rigor in many other aspects aswell. So rest assured that every gameobject has its place when my cameraposition and rotation is finally calculated and set</arrogance>
    I hate how a lot of games seem to fail on sequencing their game loop. E.g. even in Kingdom Come: Deliverance, the UI seems to update BEFORE the camera update, making gameobject-labels one the UI lag behind their targets when moving the camera.
    In Unity, the most common problem i always notice is that the devs pin the camera to Rigidbodies without smoothing - or break that smoothing by rotating the Rigidbody in Update. The second most common problem is the one you are refering to: moving camera target-objects after the camera has been set. This problem can be easily overcome by sequencing your logic correctly. I use this script for that because i seem to have much more fun doing this finicky stuff instead of actually getting anything finished :confused:
    Code (CSharp):
    1. //Version 2018-08-06
    2.  
    3. using UnityEngine;
    4. using System;
    5. using System.Collections;
    6. using System.Collections.Generic;    //List of IEnumerators
    7.  
    8. //    Marrt's TickManager
    9. //        Better timing Management for Unity
    10. //        very enhanced version of what i found here
    11. //        http://answers.unity3d.com/questions/59876/how-to-achieve-a-latefixedupdate-effect.html
    12. //
    13. //    https://docs.unity3d.com/Manual/ExecutionOrder.html
    14.  
    15. //
    16. // usage: subscribe to event within OnDisable and OnEnable
    17. //        void OnEnable(){
    18. //            TickManager.FirstTick1    += MyInputFetcher;
    19. //            TickManager.FixedTick1    += MyFixedUpdate;
    20. //            TickManager.FixedTick2    += MyLateFixedUpdate;
    21. //            TickManager.Tick1        += MyUpdate;
    22. //        }
    23. //        void OnDisable(){
    24. //            TickManager.FirstTick1    -= MyInputFetcher;
    25. //            TickManager.FixedTick1    -= MyFixedUpdate;
    26. //            TickManager.FixedTick2    -= MyLateFixedUpdate;
    27. //            TickManager.Tick1        -= MyUpdate;
    28. //        }
    29. //        private void MyInputFetcher(){
    30. //            //don't do physics stuff in here!
    31. //            //don't use deltaTime!
    32. //            //-> only prepare stuff that the next physics loop picks up,
    33. //            //        - like setting jump flag that resets after it is used in the actual physics step or
    34. //            //        - updating a moveInputVector that is used to change velocity
    35. //            //        - this is how you should do it anyway, even without this tickmanager
    36. //        }
    37. //        private void MyFixedUpdate(){ /*your previous FixedUpdate() code here*/ }
    38. //    
    39. // Order:
    40. //          1 firstTick1        // once per frame before the fixed ticks, can fetch Input before Fixed Update, happens even if no fixed Update happened this frame
    41. //          2 firstTick2
    42. //      
    43. //        Physics{    //may happen multiple times per game-loop or not at all
    44. //          3 fixedTick1;        // use this to replace FixedUpdate()
    45. //          4 fixedTick2;        // use this to replace FixedUpdate()
    46. //          5 every coroutine subscribed by AddPrePhysicsUpdateCoroutine()
    47. //          6 fixedTick3;        // use this to replace FixedUpdate()
    48. //          - INTERNAL PHYSICS UPDATE, this is where stuff is actually moved
    49. //          - OnTriggerXXX
    50. //          - OnCollisonXXX
    51. //          7 fixedCoTick1
    52. //          8 fixedCoTick2
    53. //        }
    54. //    
    55. //          9 tick1            // use this to replace Update() by subscribing a Custom function to tick1()
    56. //         10 tick2
    57. //         12 coTick1
    58. //         13 coTick2
    59. //         14 lateTick1
    60. //         15 lateTick2
    61. //         16 lateTick3
    62. //         17 EndOfFrame
    63.  
    64. public    enum TickType{
    65.     None,
    66.     FirstTickWithDeltaTime,
    67.     FirstTick1,
    68.     FirstTick2,
    69.  
    70.     FixedTick1,
    71.     FixedTick2,
    72.  
    73.     PreInternalPhysicsRoutines,
    74.  
    75.     FixedTick3,
    76.  
    77.         //INTERNAL PHYSICS UPDATE
    78.         //OnTriggerXXX
    79.         //OnCollisonXXX
    80.  
    81.     FixedCoTick1,
    82.     FixedCoTick2,
    83.  
    84.     Tick1,
    85.     Tick2,
    86.  
    87.     CoTick1,
    88.     CoTick2,
    89.  
    90.     LateTick1,
    91.     LateTick2,
    92.     LateTick3,
    93.     EndOfFrame
    94. }
    95.  
    96. /// <summary>TickManager for better control of when things happen, put this on a persistent GameObject in your startscene</summary>
    97. public class TickManager : MonoBehaviour {
    98.  
    99.     private    static    TickType    activeTickStore = TickType.None;
    100.     public    static    TickType    ActiveTick{
    101.         get            { return activeTickStore;  }
    102.         private set    { activeTickStore = value; }
    103.     }
    104.  
    105.     private    static bool firstTickSubmitted = false;
    106.     public static event Action FirstTickWithDeltaTime;    //firstTick that is created through additional Component and runs before any physics plus yielding the correct deltaTime value
    107.     public static event Action FirstTick1;        // dont use 'Time.deltaTime' here, you will get the fixed Timestep mostly! always the first event per frame, can be used to fetch input before Fixed Update in order to use it for physics without delay
    108.     public static event Action FirstTick2;        // dont use 'Time.deltaTime' here, you will get the fixed Timestep mostly! second tick for first tick, can be used if inputs at firstTick1 create changes that must still be dealt with in other scripts before physics
    109.  
    110.     public static event Action FixedTick1;        // use this to replace FixedUpdate()
    111.     public static event Action FixedTick2;        // use this to replace FixedUpdate()
    112.     //preInternalPhysicsRoutines
    113.     public static event Action FixedTick3;        // use this to replace FixedUpdate()
    114.     //INTERNAL PHYSICS UPDATE, this is where stuff is actually moved, check below WorkerTest_UpdateBEFOREPhysicsCoroutine to see how implement something that runs in coroutine before physics update unlike yield return new WaitForFixedUpdate()
    115.     //OnTriggerXXX
    116.     //OnCollisonXXX
    117.     public static event Action FixedCoTick1;    // yield return new WaitForFixedUpdate();
    118.     public static event Action FixedCoTick2;    // yield return new WaitForFixedUpdate();
    119.  
    120.     public static event Action Tick1;            // use this to replace Update()
    121.     public static event Action Tick2;
    122.     public static event Action CoTick1;
    123.     public static event Action CoTick2;
    124.     public static event Action LateTick1;
    125.     public static event Action LateTick2;
    126.     public static event Action LateTick3;
    127.  
    128.  
    129.     public static event Action EndOfFrameTick;
    130.  
    131.     public    static    float    lastFrameStartRealtime = -1F;
    132.      
    133.     void Awake () {
    134.         StartCoroutine( CoroutineTick());
    135.         StartCoroutine( FixedCoTick());
    136.         StartCoroutine( EndOfFrameCall() );
    137.  
    138.         if(debugTimingSubscriptionsByKeyH){ DebugSubscriptionsTimings(); }
    139.  
    140.         //Profiling test for AddPrePhysicsUpdateCoroutine
    141.         //for(int i = 0; i<100000; i++) {    AddPrePhysicsUpdateCoroutine( WorkerTest_UpdateBEFOREPhysicsCoroutine() );    }
    142.     }
    143.  
    144.  
    145.     private static List<IEnumerator>    preInternalPhysicsRoutines    = new List<IEnumerator>();
    146.     private static List<IEnumerator>    preInternalRemovalMarker    = new List<IEnumerator>();
    147.  
    148.     //How to get a Coroutine to run reliably BEFORE INTERNAL PHYSICS UPDATE?  
    149.     // Add your IEnumerator here 'AddPrePhysicsUpdateCoroutine()', yield by 'yield return null;'
    150.     // save a reference to your coroutine before adding, never call StartCoroutine on it:
    151.     //    
    152.     //        Start:
    153.     //        myCoroutineReference = MyCoroutine( myVar1, myVar2 );
    154.     //        AddPrePhysicsUpdateCoroutine( myCoroutineReference );
    155.     //
    156.     //        Removal:
    157.     //        After it has been finished it is automatically removed, you can also remove it manually
    158.     //        RemovePrePhysicsUpdateCoroutine( myCoroutineReference );
    159.     //        (is there a memory buildup problem i might not aware of?, do i have to end the IEnumertor explicitely somehow, profiler seems fine)
    160.     //
    161.     /// <summary> Subscribe an IEnumerator to be called BEFORE the internal physics Update. In the Coroutine, yield by 'yield return null;' </summary>
    162.     public    static    void AddPrePhysicsUpdateCoroutine    ( IEnumerator coroutine )    {    preInternalPhysicsRoutines.Add        (coroutine);    }
    163.     public    static    void RemovePrePhysicsUpdateCoroutine( IEnumerator coroutine )    {    preInternalPhysicsRoutines.Remove    (coroutine);    }
    164.  
    165.     IEnumerator WorkerTest_UpdateBEFOREPhysicsCoroutine() {
    166.         //TEST: dont start this via StartCoroutine, use AddPrePhysicsUpdateCoroutine( WorkerTest_UpdateBEFOREPhysicsCoroutine() );
    167.         int i = 0;
    168.         for(;;) {
    169.             i++; if(i == 100) { break; }
    170.             yield return null;
    171.         }
    172.     }
    173.  
    174.  
    175.     /// <summary>For Debuggung Print the amount of subscriptions that are hooked to the events</summary>
    176.     public string PrintSubCounts(){
    177.         string subscriptionInfo = "";
    178.      
    179.         subscriptionInfo +=    "firstTick\t\t"    +"["+    (FirstTick1        ==null?"000":FirstTick1.    GetInvocationList().Length.ToString("D3"))+"]\n";
    180.         subscriptionInfo +=    "firstTick\t\t"    +"["+    (FirstTick2        ==null?"000":FirstTick2.    GetInvocationList().Length.ToString("D3"))+"]\n";
    181.         subscriptionInfo +=    "fixedTick1\t\t"+"["+    (FixedTick1        ==null?"000":FixedTick1.    GetInvocationList().Length.ToString("D3"))+"]\n";
    182.         subscriptionInfo +=    "fixedTick2\t\t"+"["+    (FixedTick2        ==null?"000":FixedTick2.    GetInvocationList().Length.ToString("D3"))+"]\n";
    183.         subscriptionInfo +=    "prePhyCoro\t\t"+"["+    preInternalPhysicsRoutines.Count.ToString("D3")+"]\n";
    184.         subscriptionInfo +=    "fixedTick3\t\t"+"["+    (FixedTick3        ==null?"000":FixedTick3.    GetInvocationList().Length.ToString("D3"))+"]\n";
    185.         subscriptionInfo +=    "fixedCoTick1\t"+"["+    (FixedCoTick1    ==null?"000":FixedCoTick1.    GetInvocationList().Length.ToString("D3"))+"]\n";
    186.         subscriptionInfo +=    "fixedCoTick2\t"+"["+    (FixedCoTick2    ==null?"000":FixedCoTick2.    GetInvocationList().Length.ToString("D3"))+"]\n";
    187.         subscriptionInfo +=    "tick1\t\t"        +"["+    (Tick1            ==null?"000":Tick1.            GetInvocationList().Length.ToString("D3"))+"]\n";
    188.         subscriptionInfo +=    "tick2\t\t"        +"["+    (Tick2            ==null?"000":Tick2.            GetInvocationList().Length.ToString("D3"))+"]\n";
    189.         subscriptionInfo +=    "coTick1\t\t"    +"["+    (CoTick1        ==null?"000":CoTick1.        GetInvocationList().Length.ToString("D3"))+"]\n";
    190.         subscriptionInfo +=    "coTick2\t\t"    +"["+    (CoTick2        ==null?"000":CoTick2.        GetInvocationList().Length.ToString("D3"))+"]\n";
    191.         subscriptionInfo +=    "lateTick1\t\t"    +"["+    (LateTick1        ==null?"000":LateTick1.        GetInvocationList().Length.ToString("D3"))+"]\n";
    192.         subscriptionInfo +=    "lateTick2\t\t"    +"["+    (LateTick2        ==null?"000":LateTick2.        GetInvocationList().Length.ToString("D3"))+"]\n";
    193.         subscriptionInfo +=    "lateTick3\t\t"    +"["+    (LateTick3        ==null?"000":LateTick3.        GetInvocationList().Length.ToString("D3"))+"]\n";
    194.      
    195.         print(subscriptionInfo);
    196.         return subscriptionInfo;
    197.     }
    198.  
    199.  
    200.     //CALLS:
    201.     public    static    TickHelper tickHelper = null;
    202.  
    203.     public static void TickHelperStart( int creationFrame )
    204.     {
    205.         if(debugTimingSubscriptionsByKeyH){ print("5. called TickHelperStart  (created:"+creationFrame +") in frame:"+Time.frameCount); }
    206.         ActiveTick = TickType.FirstTickWithDeltaTime;    if(FirstTickWithDeltaTime != null)            { FirstTickWithDeltaTime(); }
    207.         CheckFirstTickSubmission();
    208.     }
    209.  
    210.     private    static void CheckFirstTickSubmission()
    211.     {
    212.         if(!firstTickSubmitted){        
    213.             ActiveTick = TickType.FirstTick1;        if(FirstTick1 != null)        { FirstTick1();    }
    214.             ActiveTick = TickType.FirstTick2;        if(FirstTick2 != null)        { FirstTick2();    }
    215.             lastFrameStartRealtime    = Time.realtimeSinceStartup;
    216.             firstTickSubmitted        = true;
    217.         }
    218.     }
    219.  
    220.     void FixedUpdate()
    221.     {
    222.         CheckFirstTickSubmission();
    223.  
    224.         ActiveTick = TickType.FixedTick1;        if(FixedTick1 != null)            { FixedTick1();    }
    225.         ActiveTick = TickType.FixedTick2;        if(FixedTick2 != null)            { FixedTick2();    }
    226.      
    227.  
    228.         ActiveTick = TickType.PreInternalPhysicsRoutines;
    229.         //manually continue all Coroutines added to preInternalPhysicsRoutines
    230.         preInternalRemovalMarker.Clear();
    231.         foreach(IEnumerator ien in preInternalPhysicsRoutines)    {    if( !ien.MoveNext()) { preInternalRemovalMarker.Add(ien);  }    }    //if(preInternalRemovalMarker.Count > 0) { print("ending something");}
    232.         foreach(IEnumerator ien in preInternalRemovalMarker)    {    preInternalPhysicsRoutines.Remove(ien);    }
    233.  
    234.         ActiveTick = TickType.FixedTick3;        if(FixedTick3 != null)            { FixedTick3();        }
    235.     }
    236.     IEnumerator FixedCoTick()
    237.     {
    238.         for(;;) {
    239.             ActiveTick = TickType.FixedCoTick1;        if(FixedCoTick1 != null)        {    FixedCoTick1();    }
    240.             ActiveTick = TickType.FixedCoTick2;        if(FixedCoTick2 != null)        {     FixedCoTick2();    }
    241.             yield return new WaitForFixedUpdate();
    242.         }
    243.     }
    244.  
    245.     void Update()
    246.     {
    247.         //if no physics Update happened and no subscriber uses FirstTickWithDeltaTime, this frame this is the first Tick
    248.         CheckFirstTickSubmission();
    249.  
    250.         ActiveTick = TickType.Tick1;        if(Tick1 != null) { Tick1(); }
    251.         ActiveTick = TickType.Tick2;        if(Tick2 != null) { Tick2(); }
    252.     }
    253.     IEnumerator CoroutineTick()
    254.     {
    255.         for(;;) {
    256.             ActiveTick = TickType.CoTick1;        if(CoTick1 != null) { CoTick1(); }
    257.             ActiveTick = TickType.CoTick2;        if(CoTick2 != null) { CoTick2(); }
    258.             yield return null;
    259.         }
    260.     }
    261.     void LateUpdate()
    262.     {
    263.         ActiveTick = TickType.LateTick1;        if(LateTick1 != null) { LateTick1();}
    264.         ActiveTick = TickType.LateTick2;        if(LateTick2 != null) { LateTick2();}
    265.         ActiveTick = TickType.LateTick3;        if(LateTick3 != null) { LateTick3();}
    266.     }
    267.  
    268.     private    IEnumerator EndOfFrameCall()
    269.     {
    270.         int lastCreationFrame = 0;
    271.  
    272.         while(true){
    273.  
    274.             ActiveTick = TickType.None;
    275.  
    276.             //Wait for the very end of the frame
    277.             yield return new WaitForEndOfFrame();
    278.  
    279.             ActiveTick = TickType.EndOfFrame;
    280.             if( EndOfFrameTick != null ) { EndOfFrameTick();}
    281.  
    282.             //catch double creations
    283.             if( lastCreationFrame == Time.frameCount ){    Debug.LogWarning("Helper called twice, may happen during Pause in Editor"); continue;    }
    284.  
    285.             //add new tickhelerp to get a new awake, NEEDS TO BE DONE IN 'EndOfFrame', otherwise Start() will be called immidiately
    286.             if( tickHelper != null ){    Destroy ( tickHelper );    }
    287.  
    288.             if( FirstTickWithDeltaTime != null ){
    289.              
    290.                 if(debugTimingSubscriptionsByKeyH){ print("1. before adding TickHelper "+Time.frameCount); }
    291.  
    292.                 //create new Component that calls its sertfasdgbvac at beginning of next frame
    293.                 tickHelper            = this.gameObject.AddComponent<TickHelper>();
    294.                 lastCreationFrame    = tickHelper.creationFrame;
    295.  
    296.                 if(debugTimingSubscriptionsByKeyH){ print("3. after adding TickHelper "+Time.frameCount); }
    297.  
    298.             }
    299.             //reset firstTick at the very end
    300.             firstTickSubmitted = false;
    301.         }
    302.     }
    303.  
    304.     public    static bool    debugTimingSubscriptionsByKeyH = false;
    305.     private    string    chronoString = "";
    306.     private    void    DebugSubscriptionsTimings(){
    307.  
    308.         KeyCode testKey = KeyCode.H;
    309.  
    310.         FirstTickWithDeltaTime    += () => {    chronoString  = "";                                                                            };
    311.         FirstTickWithDeltaTime    += () => {    chronoString += "FirstTickDeltaTime\t"    +Time.frameCount+","+Input.GetKeyDown(testKey)+", ("+Time.deltaTime +" != "+Time.fixedDeltaTime+")\n";    };
    312.         FirstTick1                += () => {    chronoString += "FirstTick1\t\t"        +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    313.         FirstTick2                += () => {    chronoString += "FirstTick2\t\t"        +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    314.         FixedTick1                += () => {    chronoString += "FixedTick1\t\t"        +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    315.         FixedTick2                += () => {    chronoString += "FixedTick2\t"            +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    316.         //                        +=                                    preInter    nalPhysicsRoutines.Count.ToString("D3")+"]\n";
    317.         FixedTick3                += () => {    chronoString += "FixedTick3\t"            +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    318.         FixedCoTick1            += () => {    chronoString += "FixedCoTick1\t"        +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    319.         FixedCoTick2            += () => {    chronoString += "FixedCoTick2\t"        +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    320.         Tick1                    += () => {    chronoString += "Tick1\t\t"                +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    321.         Tick2                    += () => {    chronoString += "Tick2\t\t"                +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    322.         CoTick1                    += () => {    chronoString += "CoTick1\t\t"            +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    323.         CoTick2                    += () => {    chronoString += "CoTick2\t\t"            +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    324.         LateTick1                += () => {    chronoString += "LateTick1\t\t"            +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    325.         LateTick2                += () => {    chronoString += "LateTick2\t\t"            +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    326.         LateTick3                += () => {    chronoString += "LateTick3\t\t"            +Time.frameCount+","+Input.GetKeyDown(testKey)+"\n";    };
    327.         LateTick3                += () => {    print(chronoString);                                        };
    328.         #if UNITY_EDITOR
    329.         LateTick3                += () => {    if(Input.GetKeyDown(testKey)){ UnityEditor.EditorApplication.isPaused = true;};                    };
    330.         #endif
    331.     }
    332.  
    333.     #region CopyPasta
    334.     //put relevant event subscriptions into the monobehaviour. unsubscribe in disable via '-=' referiung to the same Actions or Functions, caching them might be advisable
    335.  
    336.     private    System.Action    dF    = ()=>{};    //dummyFunction
    337.     private    IEnumerator        dC    = null;        //dummyCoroutine
    338.  
    339.     private void OnEnableSubscriptions(){
    340.         TickManager.FirstTickWithDeltaTime    += dF;
    341.         TickManager.FirstTick1                += dF;
    342.         TickManager.FirstTick2                += dF;
    343.  
    344.         TickManager.FixedTick1                += dF;
    345.         TickManager.FixedTick2                += dF;
    346.         TickManager.AddPrePhysicsUpdateCoroutine(dC);
    347.         TickManager.FixedTick3                += dF;
    348.         //INTERNAL PHYSICS UPDATE
    349.         //OnTriggerXXX
    350.         //OnCollisonXXX
    351.         TickManager.FixedCoTick1            += dF;
    352.         TickManager.FixedCoTick2            += dF;
    353.  
    354.         TickManager.Tick1                    += dF;
    355.         TickManager.Tick2                    += dF;
    356.         TickManager.CoTick1                    += dF;
    357.         TickManager.CoTick2                    += dF;
    358.         TickManager.LateTick1                += dF;
    359.         TickManager.LateTick2                += dF;
    360.         TickManager.LateTick3                += dF;
    361.     }
    362.     private void OnDisableUnsubscriptions(){
    363.         TickManager.FirstTickWithDeltaTime    -= dF;
    364.         TickManager.FirstTick1                -= dF;
    365.         TickManager.FirstTick2                -= dF;
    366.  
    367.         TickManager.FixedTick1                -= dF;
    368.         TickManager.FixedTick2                -= dF;
    369.         TickManager.AddPrePhysicsUpdateCoroutine(dC);
    370.         TickManager.FixedTick3                -= dF;
    371.         //INTERNAL PHYSICS UPDATE
    372.         //OnTriggerXXX
    373.         //OnCollisonXXX
    374.         TickManager.FixedCoTick1            -= dF;
    375.         TickManager.FixedCoTick2            -= dF;
    376.  
    377.         TickManager.Tick1                    -= dF;
    378.         TickManager.Tick2                    -= dF;
    379.         TickManager.CoTick1                    -= dF;
    380.         TickManager.CoTick2                    -= dF;
    381.         TickManager.LateTick1                -= dF;
    382.         TickManager.LateTick2                -= dF;
    383.         TickManager.LateTick3                -= dF;
    384.     }
    385.     #endregion CopyPasta
    386. }
    But i agree: Don't jump to conclusions because of my above findings until i post a clean demo that perfectly isolated this mystical behaviour. My intention was merely to sample if someone else ever experienced something similar.
    Edit: But maybe i did not consider the interaction between deltaTime <-> Vsync, i always assumed that Vsync will impact deltaTime correctly, is that the case?

    I think i tested it like 3 months ago
    but i wasn't able to get any conclusive results. But as far as i know Unity utilizes this when the option is enabled: https://developer.android.com/games/sdk/frame-pacing. Maybe i used the wrong API-Level or whatever.

    The most common issue on Android is that cpu-throttling starts to kill my framerate after exactly 30 seconds. Only fix is to set the visual quality low enough so that throttling does not trigger. And, like already mentioned, when my framerate drops below 60, setting the targetframerate to 120 mysteriously looks smoother even though the measured framerate stays the same.
    This mysterious behaviour can either be an issue with my game. Or android screens wait until the screen is ready before ever refreshing since there is never any screen tearing on android.
     
    Last edited: Mar 4, 2020
  15. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    doing targetFrameRate on mobile after seting Vsync have no meaning at all, no? Vsync just override the targetFramerate as far i know.

    Also, Vsync for mobile is better than using TargetFrameRate, Hardware vs Software. (I had ask the same question to Unity Company Support before).

    The new Optimize Frame for Android is a black box for me, i will try it and test it soon.
     
  16. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    True, setting it to -1 in my scripts has no function but i still do it to make it obvious for readers that the targetFramerate is essentially disabled in this state.

    I don't really understand, define "better". And what does "Hardware vs Software" mean in this context?
     
  17. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    better; less frame spike, can check using systrace capture.

    'TargetFrameRate is implemented using a software sleep(). Vsync is implemented by waiting for the GPU. VSync is superior if you can use it.'
     
  18. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    Hmm, does this explain why Application.targetFrameRate=120 looks *better when my framerate is already below 60? Shorter sleep = more chance to finish rendering before the next screen refresh?

    * better means smoother, I can absolutely tell the difference with my bare eyes. The Video i made proves it too, you can clearly see that the frames in 120 mode (40seconds mark) a spaced way more evenly (but i this tested on samsung devices only)
     
  19. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    @Marrt if you can see it with your eyes, you should be able to see it in the Profiler too no? Maybe there you could see if/how the different waiting mechanisms make a difference?
     
  20. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    How exactly should i do this? With this?: https://github.com/Over17/UnitySystracePlugin

    Or does Unity now have native stuff for On-Device profiling that i totally missed?
     
  21. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    I was mostly thinking about using the Profiler to attach to the device and then checking out how the difference maps out in timeline view. I.e. where the waiting occurs in the frame and how consistent it hits the frame rate.
     
  22. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    MartinTilo likes this.
  23. FranFndz

    FranFndz

    Joined:
    Sep 20, 2018
    Posts:
    178
    I recommend using Snapdragon profiler, the last update show frame time even when is less than the refresh rate.
    RenderDoc minimun count is refresh rate.
    Arm Streamline GPU Profiler is kinda buggy, still better than unity one, pretty sure ARM is moving to RenderDoc.

    Systrace is very simple, connect your device via usb and make sure it show up at adb, then go to systrace install folder via terminal and run the capture command.
    $ python systrace.py -o myprofile.html

    open the html file using google chrome and check for Unity Choreographer.
     
  24. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,452
    Hey, I know it's been a while but I've gathered some more understanding on this topic in the mean time and I thought I'd share that here to clarify a few things

    Where did you get these 40 FPS? is this an average over multiple frames or the timing for one frame as taken from the profiler? could it be that the phone's screen refresh rate isn't 60 Hz but rather 40 or 80? If so, setting the targetFrameRate to 30 or 60 might indeed be detrimental as this is only setting the minimum time to wait for the frame to be done. And yes, phones can only flip on a vBlank so you'd fair for 1s/set targetFrameRate + however long it takes to wait for the next vBlank. I coudn't find definitive specs for the devices you listed just now but you should be able to get the refresh rate from Screen.currentResolution.refreshRate

    That's not quite true on mobile. If Application.targetFrameRate is set to a value equal to the screen refresh rate (or that vaule devided by an integer), Unity will wait on a condition variable set by the Android “Choreographer” API. I got no info on if that is better or the same as sleeping though, just thought I'd add that detail.