Skip to content

Fix for unnecessary updates of joysticks and gamepads in GLFW backend - #2616

Open
berke-bakar wants to merge 2 commits into
dotnet:mainfrom
berke-bakar:fix/glfw-unnecessary-polling
Open

berke-bakar wants to merge 2 commits into
dotnet:mainfrom
berke-bakar:fix/glfw-unnecessary-polling

Conversation

@berke-bakar

Copy link
Copy Markdown

This PR aims to fix issue #2362 . Currently on Update method of each joystick and gamepad we are making a native call to see if the device is still connected (or if it is a gamepad). This fix aims to reduce number of native calls to only necessary ones by using cached results when applicable while employing GLFW's own public partial GlfwCallbacks.JoystickCallback SetJoystickCallback ([PinObject(PinMode.UntilNextCall)] GlfwCallbacks.JoystickCallback callback) to get notified of new connections/disconnections when they happen instead of polling on each Update.

…rom GLFW and cache connection states. Used cached state where applicable to reduce native call count.
@berke-bakar

Copy link
Copy Markdown
Author

I measured 3 timings:

  • window.DoEvents() (input update + glfwPollEvents)
  • input update
  • enumarate GamePads + Joysticks

Below is a comparison of median timings across 10 runs for 20000 frames:

Measurement main this PR change
window.DoEvents() = glfwPollEvents + input update, us/frame 5.50 2.60 -52.7%
Input update only (GlfwInputContext.ProcessEvents), us/frame 4.89 2.06 -57.8%
Enumerate input.Gamepads + input.Joysticks, us/iter 4.53 0.85 -81.3%

Here is the script i used for time measuring

using System.Diagnostics;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using Silk.NET.Input;
using Silk.NET.Input.Glfw;
using Silk.NET.Windowing;
using Silk.NET.Windowing.Glfw;

var frames = 5000;
for (var i = 0; i < args.Length; i++)
{
    if (args[i] == "--frames" && i + 1 < args.Length)
    {
        frames = int.Parse(args[++i]);
    }
}

GlfwWindowing.RegisterPlatform();
GlfwInput.RegisterPlatform();

var opts = WindowOptions.Default with { IsVisible = false, Title = "Benchmark", Size = new(320, 240) };
using var window = Window.Create(opts);
window.Initialize();
using var input = window.CreateInput();

var hooked = new HashSet<IInputDevice>();

void SubscribeToInputEvents(IInputDevice d)
{
    if (!hooked.Add(d))
    {
        return;
    }
    switch (d)
    {
        case IGamepad g:
            g.ButtonDown += (gp, b) => Console.WriteLine($"  gamepad[{gp.Index}] {b.Name} down");
            g.ButtonUp += (gp, b) => Console.WriteLine($"  gamepad[{gp.Index}] {b.Name} up");
            break;
        case IJoystick j:
            j.ButtonDown += (js, b) => Console.WriteLine($"  joystick[{js.Index}] button {b.Index} down");
            j.ButtonUp += (js, b) => Console.WriteLine($"  joystick[{js.Index}] button {b.Index} up");
            break;
    }
}

input.ConnectionChanged += (d, connected) =>
{
    Console.WriteLine($"ConnectionChanged: {d.GetType().Name}[{d.Index}] '{d.Name}' {(connected ? "connected" : "disconnected")}");
    if (connected)
    {
        SubscribeToInputEvents(d);
    }
};

var gamepads = input.Gamepads.Count;
var joysticks = input.Joysticks.Count;
Console.WriteLine($"Connected at start: {gamepads} gamepad(s), {joysticks} joystick(s)");

foreach (var g in input.Gamepads)
{
    Console.WriteLine($"-> gamepad[{g.Index}] {g.Name}"); 
    SubscribeToInputEvents(g);
}

foreach (var j in input.Joysticks)
{
    Console.WriteLine($"-> joystick[{j.Index}] {j.Name}"); 
    SubscribeToInputEvents(j);
}

var processEventsMethod = input.GetType().GetMethod("ProcessEvents", BindingFlags.Instance | BindingFlags.Public)!;
var processEvents = Expression.Lambda<Action>(
    Expression.Call(Expression.Constant(input, input.GetType()), processEventsMethod)).Compile();

// Timings
for (var i = 0; i < 500; i++)
{
    // JIT warm up + GLFW joystick subsystem init
    window.DoEvents();
}

var sw = Stopwatch.StartNew();
for (var i = 0; i < frames; i++)
{
    window.DoEvents();
}
var doEventsUs = sw.Elapsed.TotalMicroseconds / frames;

sw.Restart();
for (var i = 0; i < frames; i++)
{
    processEvents();
}
var inputUs = sw.Elapsed.TotalMicroseconds / frames;

sw.Restart();
var n = 0;
for (var i = 0; i < frames; i++)
{
    n += input.Gamepads.Count() + input.Joysticks.Count();
}
var enumUs = sw.Elapsed.TotalMicroseconds / frames;

Console.WriteLine($"{frames} frames:");
Console.WriteLine($"-> DoEvents (glfwPollEvents + input update): {doEventsUs:F2} us/frame");
Console.WriteLine($"-> input update only:                        {inputUs:F2} us/frame");
Console.WriteLine($"-> enumerate Gamepads + Joysticks:           {enumUs:F2} us/iter (n={n})");

// // Second context, register/unregister of the shared joystick callback with two live contexts.
// using (var window2 = Window.Create(opts with { Title = "Benchmark2" }))
// {
//     window2.Initialize();
//     using var input2 = window2.CreateInput();
//     for (var i = 0; i < 10; i++)
//     {
//         window2.DoEvents();
//         window.DoEvents();
//     }
//     Console.WriteLine($"Second input context OK ({input2.Gamepads.Count} gamepad(s), {input2.Joysticks.Count} joystick(s))");
// }
//
// for (var i = 0; i < 10; i++)
// {
//     window.DoEvents();
// }
// Console.WriteLine("Second context disposed, first context still updating OK");

Machine info:

  • 13th Gen Intel(R) Core(TM) i9-13900K
  • Windows build 10.0.26200.0
  • .NET SDK 8.0.418

@berke-bakar
berke-bakar marked this pull request as ready for review September 23, 2026 10:56
@berke-bakar

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant