Created
December 29, 2021 01:05
-
-
Save all-iver/8b0d8a8c56eb789f3f314d2fca2cb948 to your computer and use it in GitHub Desktop.
Disable Unity resource usage when idle, but wake instantly on input events
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// See https://devlogs.fun/projects/89-femto-paint/log/posts/254-making-a-paint-app-in-unity for context | |
using UnityEngine; | |
using UnityEngine.LowLevel; | |
using UnityEngine.InputSystem; | |
using UnityEngine.InputSystem.LowLevel; | |
/// <summary>This is a singleton to help with using Unity for application development. You have to be using the new | |
/// InputSystem for this to work (I haven't tried with legacy input). Put an instance of this in your scene. It works | |
/// by monitoring all input events, and if there hasn't been any event for a short time, it disables all unnecessary | |
/// Unity SubSystems. Because the SubSystem that listens for input events is still running, it's able to wake | |
/// instantly on input. Seems to work well on Windows, other platforms need to be tested more. | |
/// Note - Update/FixedUpdate/LateUpdate all still run when idle, but since lots of other things are turned off, your | |
/// code may need to be aware of when we're idle or not, depending on what it's expecting to happen. | |
/// </summary> | |
public class IdleLoop : MonoBehaviour | |
{ | |
/// <summary>List of SubSystems to keep running, even when idle. Note that the InputSystem's Update still needs to | |
/// get called, and delayed tasks need to run so native code is able to queue new events on the main thread. | |
/// Otherwise I'm not really sure what needs to happen but this set of SubSystems seems to work well for me when | |
/// idle.</summary> | |
public string[] runWhenIdle = new string[] { "Update+ScriptRunDelayedTasks", "FixedUpdate+NewInputFixedUpdate", | |
"PreUpdate+NewInputUpdate", "FixedUpdate+ScriptRunBehaviourFixedUpdate", "Update+ScriptRunBehaviourUpdate", | |
"PreLateUpdate+ScriptRunBehaviourLateUpdate" }; | |
/// <summary>Time in seconds with no user input before going idle.</summary> | |
public float idleSeconds = 0.25f; | |
/// <summary>When idle, we need to insert a little delay or Unity will use 100% CPU because it's no longer waiting | |
/// on VSync or GPU rendering.</summary> | |
public int msDelay = 1; | |
/// <summary>Framerate when active.</summary> | |
public int activeFramerate = 60; | |
/// <summary>Framerate when idle, just to do a frame occasionally.</summary> | |
public int idleFramerate = 1; | |
/// <summary>Unity SubSystems we'll keep running when idle. Matches the names in runInIdle.</summary> | |
public PlayerLoopSystem[] taskSubSystems { get; private set; } | |
/// <summary>Time of the last input event.</summary> | |
public float timeOfLastInput { get; private set; } | |
/// <summary>Time of the last frame we rendered, used to manage framerate when idle.</summary> | |
public float timeOfLastFrame { get; private set; } | |
/// <summary>The time in seconds we'll try to keep per frame, to manage framerate when idle</summary> | |
public float frameTime { get { return 1f / idleFramerate; } } | |
/// <summary>Are we idle or not?</summary> | |
public bool isIdle { get; private set; } | |
/// <summary>The current FPS, for debug purposes. Calculated by adding the number of times our LateUpdate() is | |
/// called when actively rendering.</summary> | |
public int fps { get; private set; } | |
/// <summary>Time when we last calculated fps, for debug purposes.</summary> | |
float timeOfLastFPSUpdate; | |
/// <summary>Number of frames since the last time we calculated fps, for debug purposes.</summary> | |
int frameCount; | |
static IdleLoop _instance; | |
/// <summary>Get the singleton instance of this class.</summary> | |
public static IdleLoop instance { | |
get { | |
if (_instance == null) { | |
_instance = FindObjectOfType<IdleLoop>(); | |
if (_instance == null) | |
throw new System.Exception("IdleLoop not found in scene"); | |
} | |
return _instance; | |
} | |
} | |
void Awake() { | |
// these next two aren't really necessary to make this technique work, so remove them if you don't want them | |
QualitySettings.vSyncCount = -1; | |
Application.targetFrameRate = activeFramerate; | |
// runInBackground = false isn't necessary to do, but it probably makes sense for applications | |
// NOTE - in Linux, runInBackground doesn't work and instead of going to 0, CPU usage goes to 100%. verified | |
// on a new empty project. | |
Application.runInBackground = false; | |
// populate taskSubSystems based on the names in runWhenIdle | |
var loop = PlayerLoop.GetCurrentPlayerLoop(); | |
taskSubSystems = new PlayerLoopSystem[runWhenIdle.Length]; | |
RecursePlayerLoop(loop, 0); | |
for (int i = 0; i < taskSubSystems.Length; i++) { | |
if (taskSubSystems[i].type == null) { | |
Debug.LogError("Task SubSystem " + runWhenIdle[i] + " not found in PlayerLoop"); | |
// enabled = false; | |
} | |
} | |
timeOfLastInput = Time.realtimeSinceStartup; | |
InputSystem.settings.updateMode = InputSettings.UpdateMode.ProcessEventsInDynamicUpdate; | |
InputSystem.onEvent += OnInputEvent; | |
} | |
/// <summary>Called by InputSystem when an input event is received.</summary> | |
void OnInputEvent(InputEventPtr eventPtr, InputDevice device) { | |
timeOfLastInput = Time.realtimeSinceStartup; | |
if (isIdle) | |
SetActive(); | |
} | |
/// <summary>Recurse through the PlayerLoop looking for all the SubSystems in runWhenIdle, and add an entry to | |
/// taskSubSystems for each one found.</summary> | |
void RecursePlayerLoop(PlayerLoopSystem loop, int nests) { | |
if (loop.type != null) { | |
for (int i = 0; i < runWhenIdle.Length; i++) { | |
if (loop.type.ToString().Contains(runWhenIdle[i])) | |
taskSubSystems[i] = loop; | |
} | |
} | |
if (loop.subSystemList == null) | |
return; | |
foreach (var subLoop in loop.subSystemList) | |
RecursePlayerLoop(subLoop, nests + 1); | |
} | |
/// <summary>Turn off all SubSystems that aren't in runWhenIdle.</summary> | |
void SetIdle() { | |
var loop = new PlayerLoopSystem(); | |
loop.subSystemList = taskSubSystems; | |
PlayerLoop.SetPlayerLoop(loop); | |
isIdle = true; | |
} | |
/// <summary>Restore Unity's default player loop.</summary> | |
void SetActive() { | |
PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop()); | |
isIdle = false; | |
} | |
void LateUpdate() { | |
var realtimeSinceStartup = Time.realtimeSinceStartup; | |
if (isIdle) { | |
if (realtimeSinceStartup - timeOfLastFrame >= frameTime) { | |
// it's time to render a frame, so turn off idle. this will only apply for one render since the next | |
// time we'll set to idle again if there haven't been any input events. | |
SetActive(); | |
} else { | |
// blocking here gives the CPU a break since without waiting for the GPU the CPU is never idle. | |
// FIXME - this doesn't seem to quite work on Linux and the CPU usage is still 20-30%. | |
System.Threading.Thread.Sleep(msDelay); | |
} | |
} else { | |
// we're not idle, this is an actual render. calculate the FPS. | |
timeOfLastFrame = realtimeSinceStartup; | |
frameCount ++; | |
if (realtimeSinceStartup - timeOfLastFPSUpdate >= 1) { | |
fps = frameCount; | |
frameCount = 0; | |
timeOfLastFPSUpdate = realtimeSinceStartup; | |
} | |
// if we haven't had any input events and are over the threshold, go idle. | |
if (realtimeSinceStartup - timeOfLastInput >= idleSeconds) | |
SetIdle(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment