Skip to content

Error creating multiple windows in .NET 8 #2436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Av3boy opened this issue Mar 24, 2025 · 9 comments
Open

Error creating multiple windows in .NET 8 #2436

Av3boy opened this issue Mar 24, 2025 · 9 comments
Labels
bug Something isn't working

Comments

@Av3boy
Copy link

Av3boy commented Mar 24, 2025

Summary

I wanted to build a docking system using ImGui where, when the imgui window is undocked another silk.net window is created to host that tabs content. Just running the window was not possible in the same thread since the Run method would block the "main" window from running. After that I tried to move the new window into it's own thread and I noticed that something wasn't working since I was getting the following error:

Silk.NET.GLFW.GlfwException: 'PlatformError: Win32: Failed to register window class: Class already exists. '

I tried to reproduce the problem in a test project and using .NET 6 this works just fine. However using .NET 8 like the rest of the project, I got the same error using this code (NOTE: The 'usafe' block is just for debugging purposes, doesn't affect the problem itself):

    static void Main()
    {
       // Changing the WindowClass did not work.
        var options1 = WindowOptions.Default with { Title = "Window 1", WindowClass = "MyWindowClass1" };
        var options2 = WindowOptions.Default with { Title = "Window 2", WindowClass = "MyWindowClass2" };

        var window1 = Window.Create(options1);
        var window2 = Window.Create(options2);

        StartWindow(window1);
        StartWindow(window2); // You can uncomment this to see the program working with just one window.
    }

    static void StartWindow(IWindow window)
    {
        var thread = new Thread(() =>
        {
            window.Load += () =>
            {
                var gl = GL.GetApi(window);

                unsafe
                {
                    var versionPtr = gl.GetString(StringName.Version);
                    string? version = SilkMarshal.PtrToString((nint)versionPtr);
                    Console.WriteLine($"OpenGL version: {version}");
                }

            };

            window.Run(); // The GlfwException is thrown here
        });

        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
    }

System info:

  • Platform: Desktop, Windows 11, Insider Preview 10.0.26120.1930
  • Framework Version: .NET 8
  • IDE: Visual Studio Community 2022 Preview, 17.14.0 Preview 2.0
  • API: OpenGL
  • API Version: OpenGL version: 3.3.0 NVIDIA 572.16
@Perksey
Copy link
Member

Perksey commented Mar 24, 2025

Using windowing on multiple threads is illegal. You must only call Silk.NET.Windowing functions on the thread that called your program’s Main function.

@Av3boy
Copy link
Author

Av3boy commented Mar 25, 2025

Why is that? To my understanding OpenGL doesn't care about the thread it's running in, only the context it uses for it's calls. A quick google gave a few articles how multithreading windowing should be techincally possible:
https://learn.microsoft.com/en-us/windows/win32/procthread/creating-windows-in-threads
https://community.khronos.org/t/multiple-opengl-windows-in-one-application/72971/3

The contexts are already handled by the IWindow implementations through the GLContext in IGLContextSource and similarly SharedContext in IWindowProperties?

@Av3boy
Copy link
Author

Av3boy commented Mar 25, 2025

Though let's assume that you are correct and instead of a multithreaded approach, could we expose the Run(Action onFrame) / Run() methods in a way that the developer could handle the game loop themselves? For example, instead of:

// ViewImplementationBase.cs
// Game loop implementation
public virtual void Run(Action onFrame)
{
    while (!IsClosing)
    {
        onFrame();
    }
}

we would do something similar to this (of course keeping the above the default):

public virtual void Run(Action onFrame)
{
    onFrame();
}

@michaeldawteck
Copy link

This is what I've been doing to run multiple windows without blocking the main thread. It's being done in a directx12-based application, but I don't think that matters for the windowing portion.

If you call Initialize on the window, but not Run, you can process it's events without blocking the main thread using DoEvents, etc.

The code below is a minimal version of what I've been working on. After the code is a little video showing it in action.
It works for opening windows on the main thread, but I'd really appreciate if @Perksey could lend his expertise on whether or not there is anything wrong with this method, in general.

Using this strategy, you can spawn long-running threads that perform other workloads (command-recording, etc.). You can ferry commands back to the main thread using a Queue similar to what I've done for creating windows. You'll see the console print out the Running loop on background thread... once per second in this case.

Like I said, it's a minimal version, so all I've done is expose the Click method and whenever the user clicks on a window, a new window appears....

using System.Collections.Concurrent;
using Silk.NET.Input;
using Silk.NET.Maths;
using Silk.NET.Windowing;

namespace System;

public static partial class Program
{
	internal static partial Task InitialzeGenerated();
	private static Task? runTask = null;
	private static readonly List<IWindow> windows = [];
	private static readonly List<IInputContext> inputContexts = [];
	private static readonly ConcurrentQueue<WindowOptions> windowQueue = [];
	private static readonly CancellationTokenSource cancellationTokenSource = new();

	public static void Main(string[] _)
	{
		runTask = Task.Run(async () =>
		{
			await Task.Delay(1000);
			windowQueue.Enqueue(WindowOptions.Default);
			while(!cancellationTokenSource.IsCancellationRequested)
			{
				await Task.Delay(1000);
				Console.WriteLine("Running loop on background thread...");
			}
		});
		while (!cancellationTokenSource.IsCancellationRequested)
		{
			foreach (var item in windows)
			{
				if (item is null) continue;
				var window = item;
				window.DoEvents();
				window.DoUpdate();
				window.DoRender();
				if (window.IsClosing)
				{
					window.Reset();
					window.Dispose();
					windows.Remove(window);
					if (windows.Count == 0)
						cancellationTokenSource.Cancel();
					break;
				}
			}
			if (!cancellationTokenSource.IsCancellationRequested)
			{
				while(windowQueue.TryDequeue(out var options))
				{
					options.Title = "new window";
					options.Position = new Vector2D<int>(
						x: 500 + (50 * windows.Count), 
						y: 400 + (50 * windows.Count));
					var window = Window.Create(options);
					window.Initialize();
					var inputContext = window.CreateInput();
					foreach (var mouse in inputContext.Mice)
						mouse.Click += Mouse_Click;
					inputContexts.Add(inputContext);
					windows.Add(window);
				}
			}
		}
	}

	private static void Mouse_Click(IMouse arg1, MouseButton arg2, Numerics.Vector2 arg3)
	{
		windowQueue.Enqueue(WindowOptions.Default);
	}
}
20250326-0104-20.7981625.mp4

@Marco-Zechner
Copy link

But isn't the main thread still "blocked" / used for rendering.
If I were to block the main thread myself because I need to do stuff that will block it for a few seconds, wouldn't all the windows freeze?

@Av3boy
Copy link
Author

Av3boy commented Mar 27, 2025

@Marco-Zechner Yes, though you can work around that. Here's what @michaeldawteck was doing with the workaround but in a simplified format for demonstration purposes:

// Start the task that adds the first window into the queue and keeps the application alive.
StartWindowQueueTask();

// NOTE: This task is the change needed to unblock the main thread.
// Unblock the main thread to handle the window lifecycle synchronously.
Task.Run(() =>
{
    while (!_cancellationTokenSource.IsCancellationRequested)
    {
        for (int i = 0; i < _windows.Count; i++)
            UpdateWindow(ref i); // Render etc
        
        // Create any new windows created during the previous iteration
        DequeueWindows();
    }
});

// See the main thread actually does stuff while the windows are being updated by the task above.
Console.WriteLine("Hello World!");

// Wait for user input to exit the main loop and exit the application.
// This can be some other "main loop" depending on your needs
Console.ReadLine();

@Perksey
Copy link
Member

Perksey commented Mar 27, 2025

Why is that? To my understanding OpenGL doesn't care about the thread it's running in, only the context it uses for it's calls. A quick google gave a few articles how multithreading windowing should be techincally possible:

This isn't about OpenGL, this is about the requirements of the operating system's windowing system. Your main thread has to be the one calling Run. I'd generally recommend calling Run(Action) on your main window, and if you have additional windows calling the DoEvents/DoUpdate/DoRender calls within that callback for all of your windows.

Now, obviously OpenGL contexts are thread-specific, and if you followed this approach exactly you'd be losing a lot of performance on constantly calling MakeCurrent for your windows. But remember, the threading requirement has nothing to do with OpenGL, you can run OpenGL wherever you want as long as the thread is consistent (i.e. you use a Thread and not a Task that runs on an undefined thread. So you can use IsContextControlDisabled to stop us pulling the render thread back to the main thread, and then use MakeCurrent, DoUpdate, and DoRender on a new Thread to move your rendering to a completely separate thread. Initialize, DoEvents, and Reset must always be called on the main thread regardless, but the others just fire their respective events and can be on the thread where your context is current. With this, your main thread is relegated to only dealing with window events. Now, it's important to note that changing any properties on the window does need to be done on the main thread (for which we have window.Invoke to access that thread from other threads). You could ever go further and setup many invisible windows, and create a pool of OpenGL contexts that share resources with your main OpenGL context to have proper multi-threaded rendering.

Hope this helps.

@dotnet dotnet deleted a comment from MarcoZechner Apr 16, 2025
@dotnet dotnet deleted a comment Apr 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
Status: Todo
Development

No branches or pull requests

4 participants