Skip to content

Instantly share code, notes, and snippets.

@all-iver
Created December 29, 2021 01:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save all-iver/8b0d8a8c56eb789f3f314d2fca2cb948 to your computer and use it in GitHub Desktop.
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
// 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