Reduce OpenGL GPU utilization by rate-limiting redraws

The GAL draw panel was rendering far more frames than needed, causing
100% GPU utilization on systems where vsync was unavailable or disabled.
Three changes address this:

Refresh() now enforces a minimum period between renders. With vsync
active the guard is 3ms (the driver throttles via SwapBuffers). Without
vsync a 16ms floor caps rendering at 60 FPS, preventing GPU saturation.

DoRePaint() tracks the cursor position and skips the entire GL cycle
when neither the view is clean and the cursor doesn't move

OnEvent() delegates directly to the rate-limited Refresh() instead of
maintaining its own adaptive throttle with a RequestRefresh() idle
fallback that fired immediately and provided no actual throttling.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/19112
This commit is contained in:
Seth Hillbrand
2026-02-17 10:53:23 -08:00
parent d5607a9864
commit b728c08b7a
2 changed files with 42 additions and 16 deletions
+39 -16
View File
@@ -263,6 +263,20 @@ bool EDA_DRAW_PANEL_GAL::DoRePaint()
cntUpd.Stop();
VECTOR2D cursorPos = m_viewControls->GetCursorPosition();
bool viewDirty = m_view->IsDirty();
bool cursorMoved = ( cursorPos != m_lastCursorPosition );
// Skip the entire GL cycle when nothing has changed. The front buffer
// still shows the previous frame so there is nothing to redraw.
if( !viewDirty && !cursorMoved )
{
m_lastRepaintEnd = wxGetLocalTimeMillis();
return true;
}
m_lastCursorPosition = cursorPos;
// GAL_DRAWING_CONTEXT can throw in the dtor, so we need to scope
// the full lifetime inside the try block
{
@@ -280,9 +294,9 @@ bool EDA_DRAW_PANEL_GAL::DoRePaint()
m_gal->SetGridColor( settings->GetGridColor() );
m_gal->SetCursorColor( settings->GetCursorColor() );
// TODO: find why ClearScreen() must be called here in opengl mode
// and only if m_view->IsDirty() in Cairo mode to avoid display artifacts
// when moving the mouse cursor
// OpenGL double-buffering leaves the back buffer undefined after
// SwapBuffers, so a full clear is always required before compositing.
// Cairo only needs to clear when NONCACHED content changed.
if( m_backend == GAL_TYPE_OPENGL )
m_gal->ClearScreen();
@@ -306,7 +320,7 @@ bool EDA_DRAW_PANEL_GAL::DoRePaint()
isDirty = true;
}
m_gal->DrawCursor( m_viewControls->GetCursorPosition() );
m_gal->DrawCursor( cursorPos );
cntCtxDestroy.Start();
}
@@ -398,8 +412,26 @@ void EDA_DRAW_PANEL_GAL::RequestRefresh()
void EDA_DRAW_PANEL_GAL::Refresh( bool aEraseBackground, const wxRect* aRect )
{
if( !DoRePaint() )
RequestRefresh();
wxLongLong now = wxGetLocalTimeMillis();
wxLongLong delta = now - m_lastRepaintEnd;
// When vsync is available the driver throttles SwapBuffers, so we only need
// a small guard to avoid queueing work faster than the GPU can consume it.
// Without vsync, enforce a 60 FPS ceiling to prevent saturating the GPU.
int minPeriodMs = 3;
if( m_gal && m_gal->IsInitialized() && m_gal->GetSwapInterval() == 0 )
minPeriodMs = 16;
if( delta >= minPeriodMs )
{
if( !DoRePaint() )
RequestRefresh();
}
else if( !m_refreshTimer.IsRunning() )
{
m_refreshTimer.StartOnce( ( minPeriodMs - delta ).GetValue() );
}
}
@@ -597,16 +629,7 @@ void EDA_DRAW_PANEL_GAL::OnEvent( wxEvent& aEvent )
else
m_eventDispatcher->DispatchWxEvent( aEvent );
// Give events time to process, based on last render duration
wxLongLong endDelta = wxGetLocalTimeMillis() - m_lastRepaintEnd;
long long timeLimit = ( m_lastRepaintEnd - m_lastRepaintStart ).GetValue() / 5;
timeLimit = std::clamp( timeLimit, 3LL, 150LL );
if( endDelta > timeLimit )
Refresh();
else
RequestRefresh();
Refresh();
}
+3
View File
@@ -288,6 +288,9 @@ protected:
/// True when canvas needs to be refreshed from idle handler
bool m_needIdleRefresh;
/// Last cursor position sent to GAL for drawing
VECTOR2D m_lastCursorPosition;
/// Interface for drawing objects on a 2D-surface
KIGFX::GAL* m_gal;