juce-framework / JUCE

JUCE is an open-source cross-platform C++ application framework for desktop and mobile applications, including VST, VST3, AU, AUv3, LV2 and AAX audio plug-ins.
https://juce.com
Other
6.38k stars 1.69k forks source link

Unusually high CPU usage in (empty) JUCE OpenGL application #945

Open Intrets opened 2 years ago

Intrets commented 2 years ago

When playing around with JUCE and OpenGL I noticed my program was doing essentially nothing but showed high CPU usage. Trying from a fresh and latest version of JUCE, an empty project created from projucer or the demo apps showed the same high CPU usage.

System: OS: Windows 10 home build 19043 CPU: Ryzen 3700x GPU: GTX 1660 super, driver 471.68 Visual Studio: 16.11.2

Reproducing steps:

  1. Download windows release from https://github.com/juce-framework/JUCE/releases/tag/6.1.0 (at time of testing and writing, the release is up to date with develop/master branches)
  2. Unzip to a folder $Foo, run Projucer, File -> Global Paths -> set Path to JUCE to $Foo/JUCE, set JUCE modules to $Foo/JUCE/modules
  3. Create new project -> OpenGL
  4. Open project in Visual Studio 2019
  5. Build and run the standalone executable (either Debug and Release will exhibit the CPU usage)

Expected result: Very minimal CPU usage by the application.

Actual result: The application completely saturates one core and then some. This shows in task manager, the Visual Studio profiler, and while breaking the program with the debugger at an arbitrary time the execution of the program is guaranteed to be in the same function.

Intrets commented 2 years ago

OK so this has to do with the graphics driver (I have read only nvidia does it this way?) spinlocking for synchronization.

Taking a look at how GLFW handles this: https://github.com/glfw/glfw/blob/63da04e5ced93fcb87a20513acdff5d78b1166ff/src/wgl_context.c#L321-L342, seems to be a good solution (stood the test of time) even though it is called a hack.

Bare minimum to get it working: https://github.com/Intrets/JUCE/commit/c942217c39299f0011d8a8c6c73f705ce84103df

SamuelBeland commented 2 years ago

@Intrets

Try switching off continuous repainting with OpenGLContext::setContinuousRepainting(bool).

Continuous repainting will always saturate the rendering thread, either by doing actual work or just by sleeping (so that it doesn't hog the Message Manager by constantly locking it).

See the body of juce::OpenGLContext::CachedImage::renderFrame()

// ...

// This avoids hogging the message thread when doing intensive rendering.
if (lastMMLockReleaseTime + 1 >= Time::getMillisecondCounter())
    Thread::sleep (2);

// ...
jjYBdx4IL commented 2 years ago

https://github.com/juce-framework/JUCE/blob/master/modules/juce_opengl/opengl/juce_OpenGLContext.cpp#L351

OpenGLContext::deactivateCurrentContext();

Is there any reason why there are the context activations inside renderFrame? As far as I can see that's already handled directly in the render thread job loop.

Apart from that, the dwmapi hint works wonders - also for timed repaints.

BTW: continuous repainting should never saturate the thread because there is swap interval of 1 by default, ie. VSYNC, so it's throttled by the frame rate of the monitor.