glCopyImageSubData with explicit version hints and secondary context breaks rendering on primary context

Specifying the GL context version breaks glCopyImageSubData on secondary contexts. The copying works just fine but everything rendered AFTER the copy does not work correctly.
I have checked the selected GL context version if I do not specify hints, and it is 4.6, so specifying the hints should make no difference.

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);

I have a more detailed question on stackoverflow, my program is in java but it just redirects to C functions so it shouldn’t make any difference.
I thought I also post here because it might be a glfw issue, idk.

My goal is to try to understand what is happening and fix it, I do not want a workaround because I wouldn’t learn anything :blush:
Thanks for help!

Have you tried investigating with a graphics debugger such as RenderDoc or one from the vender of the GPU you are using?

No I haven’t, I can’t seem to get RenderDoc to work.
Also tried intel graphics analyzer, but can’t get it to work with java
My question again: Does specifying the exact OpenGL context version that would be auto-selected make ANY difference to letting GLFW auto-select the version?
Also, glCopyImageSubData is OpenGL 4.3
If I specify versions below 4.3 (e.g. 4.2 or 1.0), everything works just fine.
I guess this is because of the forward compatibility, but why is it that if I specify a version below the one I need, it works? That all seems like a very weird glfw bug to me, idk.

Specifying OpenGL version for context creation is just a simple means to get guaranteed set of functionality from OpenGL spec available to you. Additionally to functionality of core spec you always have extra functionality available from OpenGL extensions. So if you create <4.3 version where glCopyImageSubData does not exist, it may be still available if driver exposes ARB_copy_image extension, because it also provides glCopyImageSubData function. It is normal and expected situation. Typically driver provides most of higher GL version functionality as extensions to lower GL versions. Note that sometimes there are minor differences for same functionality from core spec and extension. So you should check carefully if what you’re using is available the same in both places.

Sorry for taking my time replying, this problem has been bothering me for 2 months and I had to take a break.

So specifying the OpenGL version for context creation is just a guarantee. That would imply, that I could leave it out, create my OpenGL context and query the version from that, then check if it is high enough for my case.
In my test I query the OpenGL version after context creation and print it to the console
glGetInteger(GL_MAJOR_VERSION) + "." + glGetInteger(GL_MINOR_VERSION)
If I leave out the explicit version request, this prints 4.6
But if I specify 4.6 in the version request, as in my initial post, the functionality somehow changes and it breaks rendering (after the glCopyImageSubData call in the other (secondary) context (on another thread), created with same window hints ofc). Still prints 4.6 though.

Funny thing, don’t know if it helps, if I let my program exit normally, everything related to graphics on the entire computer freezes for about a second. If I kill the program, this does not happen. Don’t know if this has anything to do with this or helps but it also only happens if I specify the version and let the program break after the glCopyImageSubData call

Sure, leaving the version out is an option for normal OpenGL, but then talk about OpenGL ES.
There, I have to specify the version, if I want my functionality, so leaving the version out is not an option.

I know this post is kind of all over the place, pretty good reflection of my brain in fact, if I need to clarify anything at all, please ask.

Thanks for getting back to us!

I highly doubt this is in any way related to GLFW, since GLFW simply passes the version to the driver as an attribute and has no code which is dependent on the context major/minor versions on Windows (except for some error handling).

I had the time to look at your program and I note that your program is multithreaded and it runs the GLFW init an different thread to the main thread. This is not supported - Most GLFW functions must only be called from the main thread (the thread that calls main), but some may be called from any thread once the library has been initialized .

When encountering issues like this I would also simplify your program to the most basic, and remove any threading first - accidentally setting the context current on one thread whilst using it on another can cause unexpected behaviour.

So when I specify a context version, the only difference in glfw is that it is passed to the driver. If I do not specify a specific version, the driver auto-picks a version?
If that is true, then this might be a bug with the intel graphics drivers?

I rewrote the code so that all the glfw functions are in the “thread that calls main”. No difference. I thought btw, that if I keep all code with the “only from main thread” limitation in the same thread that calls glfwInit, that it doesn’t make a difference? Cause if it did, that would mean that it is dependent on the initial thread of the process, which would not make any sense to me.

I used to work at Intel as one of their game dev engineers, so I know that driver bugs do exist but that they are also very rare. The most frequent issues encountered are due to issues in the developers code which aren’t obvious due to the complexity of the API and in particular the difficulty of getting documentation which covers all circumstances.

Many GLFW functions do need to be called from the initial thread of the process, and this is due to OS requirements on some systems.

I would recommend testing your code with no threads at all first - i.e. no glfw, render and async threads at all just performing your operations in a simple loop one after the other. Given that you can only perform context operations on one thread at a time this will be of similar performance. The part you should move to another thread would be the copy of the image data to the BufferedImage after calling glGetTexImage.

If you can get the single threaded case working you might then want to investigate my usercontext PR which enables multithreaded OpenGL calls by permitting the developer to create secondary contexts to submit/retrieve data from the GPU. This will hopefully be included in the next release of GLFW as otherwise you’ll need to build/update GLFW and lwgl bindings.

Your suggestion with testing the code without threading gave me some ideas. I already know that it works without threading, the problems only arrived after I introduced threading. So I experimented a bit and found out that the order of things to break seems to have to be like this (The numbers have to happen in the correct order, multiple actions in one number don’t have to happen in the correct order):

1 → Make context 1 current on thread 1, make context 2 current on thread 2, Thread 1 waits for Thread 2 to signal
2 → Thread 2: Do whatever, call glCopyImageSubData at some point (glFlush and glFinish also called, just to be sure)
3 → Thread 2 signals Thread 1
4 → Thread 1 renders, not working

After step 3, if Thread 1 calls glfwMakeContextCurrent(glfwGetCurrentContext()), everything works just fine. If Thread 1 calls glfwMakeContextCurrent again, you can also leave step 1 for Thread 1 out (obviously). Now I’m gonna be honest, this is way out of my league I’m just doing this as a hobby, and I have no idea what’s going on anymore. The glfw managing thread can be any thread (1, 2 or some other thread).
Also experimented a little with glfwMakeContextCurrent(0) (On the non-rendering async thread) and it does not seem to make any difference at what point in time it is called (Before or after rendering)
Sorry to keep bothering .-.

Note that glfwMakeContextCurrent(glfwGetCurrentContext()) should not do anything - it gets the window whose context is current on the calling thread and then sets that window context current (except it already is).

From the code it does appear that you might be calling glGetTexImage on a texture which had not been initialized with any data (when mode == 0 for example) and this might sufficiently unusual to be an area where the driver has problems.

I do not know anything about LWJGL, so I don’t know if there is any interaction with it and the thread context, and whether this could cause any issues. It’s also difficult to decipher what you are trying to do from the code. So I do not think I can give any further advice other than to suggest sticking with an approach which works. If you are interested in further analysing what might be causing the problem you could try writing this in C/C++ in order to be able to debug the problem with fewer working parts.

Probably gonna hate me for doing this again. ._.
Problem also in C++, resizing the window can cause it to sometimes render.
This video shows this
Second video showing the difference with glfwMakeContextCurrent(glfwGetCurrentContext())

#include <iostream>
#include <thread>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <glad\glad.h>
#include <GLFW/glfw3.h>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void render(GLFWwindow* window);
int fbw = 0;
int fbh = 0;

class CountDownLatch {
public:
	explicit CountDownLatch(const unsigned int count) : m_count(count) { }
	virtual ~CountDownLatch() = default;

	void await(void) {
		std::unique_lock<std::mutex> lock(m_mutex);
		if (m_count > 0) {
			m_cv.wait(lock, [this]() { return m_count == 0; });
		}
	}

	template <class Rep, class Period>
	bool await(const std::chrono::duration<Rep, Period>& timeout) {
		std::unique_lock<std::mutex> lock(m_mutex);
		bool result = true;
		if (m_count > 0) {
			result = m_cv.wait_for(lock, timeout, [this]() { return m_count == 0; });
		}

		return result;
	}

	void countDown(void) {
		std::unique_lock<std::mutex> lock(m_mutex);
		if (m_count > 0) {
			m_count--;
			m_cv.notify_all();
		}
	}

	unsigned int getCount(void) {
		std::unique_lock<std::mutex> lock(m_mutex);
		return m_count;
	}

protected:
	std::mutex m_mutex;
	std::condition_variable m_cv;
	unsigned int m_count = 0;
};

CountDownLatch thread1latch = CountDownLatch(1);

void thread1(GLFWwindow* window) {
	std::cout << "Thread1" << std::endl;
	glfwMakeContextCurrent(window);

	GLuint id1;
	GLuint id2;
	glGenTextures(1,&id1);
	glGenTextures(1,&id2);


	glBindTexture(GL_TEXTURE_2D, id1);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

	glBindTexture(GL_TEXTURE_2D, id2);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 3, 3, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

	glCopyImageSubData(id1, GL_TEXTURE_2D, 0, 0, 0, 0, id2, GL_TEXTURE_2D, 0, 0, 0, 0, 2, 2, 1);

	glDeleteTextures(1, &id1);
	glDeleteTextures(1, &id2);

	glFlush();
	glFinish();
	thread1latch.countDown();

	glfwMakeContextCurrent(NULL);
}


int main() {
	std::cout << "Hello" << std::endl;
	
	glfwInit();

	glfwDefaultWindowHints();
	glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
	glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);

	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);


	GLFWwindow* window = glfwCreateWindow(400,400,"test",nullptr,nullptr);
	GLFWwindow* secondary = glfwCreateWindow(1,1,"unused",nullptr,window);

	glfwGetFramebufferSize(window, &fbw,&fbh);
	glfwSetFramebufferSizeCallback(window,framebuffer_size_callback);

	glfwMakeContextCurrent(window);
	glfwShowWindow(window);

	gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

	std::thread th (thread1, secondary);

	thread1latch.await();

	while (!glfwWindowShouldClose(window)) {
		glfwWaitEventsTimeout(0.1);
		glfwPollEvents();

		render(window);
	}

	th.join();

	glfwMakeContextCurrent(NULL);

	glfwDestroyWindow(secondary);
	glfwDestroyWindow(window);

	glfwTerminate();
}

void render(GLFWwindow* window) {
	glViewport(0, 0, fbw, fbh);
	glClearColor(1, 0, 0, 1);
	glClear(GL_COLOR_BUFFER_BIT);
	glfwSwapBuffers(window);
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	fbw = width;
	fbh = height;
	render(window);
}

I don’t know if this is actual problem or not - but OpenGL function pointers you load dynamically can be context specific. That means you need to do “glfwGetProcAddress” for functions you use in each context separately. Glad v1 does not support this. Glad v2 does support multi-context function pointer loading: https://gen.glad.sh - there’s a checkbox with mx for this.

I think I know what is going on.

I note that you’ve got render(window); in your framebuffer_size_callback function. It’s possible this is getting called for the secondary window on opening, even if not visible, and that glfwSwapBuffers(window); is having some interaction with the context at the driver level since GLFW calls SwapBuffers(window->context.wgl.dc);:

The callback will be called on the thread which calls glfwPollEvents(); so glfwSwapBuffers(window); with the secondary window will potentially be called on this thread (you can debug to check).

GLFW itself doesn’t explicitly set the context here, but internally WGL or the driver might, so glfwGetCurrentContext() will return the primary window and setting this current will reset the underlying context back to primary window.

So remove render(window); from your callback - you don’t need it since the next line after the poling of events is to render the window. You’ll also need to not update fbw and fbh unless window is equal to the primary window.

Glad 2, same problem

#include <iostream>
#include <thread>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <glad\gl.h>
#include <GLFW/glfw3.h>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void free_context(GladGLContext* gl);
void render(GLFWwindow* window, GladGLContext* gl);
GladGLContext* create_context(GLFWwindow* window);
int fbw = 0;
int fbh = 0;
GladGLContext* mgl;

class CountDownLatch {
public:
	explicit CountDownLatch(const unsigned int count) : m_count(count) { }
	virtual ~CountDownLatch() = default;

	void await(void) {
		std::unique_lock<std::mutex> lock(m_mutex);
		if (m_count > 0) {
			m_cv.wait(lock, [this]() { return m_count == 0; });
		}
	}

	template <class Rep, class Period>
	bool await(const std::chrono::duration<Rep, Period>& timeout) {
		std::unique_lock<std::mutex> lock(m_mutex);
		bool result = true;
		if (m_count > 0) {
			result = m_cv.wait_for(lock, timeout, [this]() { return m_count == 0; });
		}

		return result;
	}

	void countDown(void) {
		std::unique_lock<std::mutex> lock(m_mutex);
		if (m_count > 0) {
			m_count--;
			m_cv.notify_all();
		}
	}

	unsigned int getCount(void) {
		std::unique_lock<std::mutex> lock(m_mutex);
		return m_count;
	}

protected:
	std::mutex m_mutex;
	std::condition_variable m_cv;
	unsigned int m_count = 0;
};

CountDownLatch thread1latch = CountDownLatch(1);

void thread1(GLFWwindow* window) {
	std::cout << "Thread1" << std::endl;
	GladGLContext* gl = create_context(window);

	GLuint id1;
	GLuint id2;
	gl->GenTextures(1,&id1);
	gl->GenTextures(1,&id2);


	gl->BindTexture(GL_TEXTURE_2D, id1);
	gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	gl->TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

	gl->BindTexture(GL_TEXTURE_2D, id2);
	gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	gl->TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 3, 3, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

	gl->CopyImageSubData(id1, GL_TEXTURE_2D, 0, 0, 0, 0, id2, GL_TEXTURE_2D, 0, 0, 0, 0, 2, 2, 1);

	gl->DeleteTextures(1, &id1);
	gl->DeleteTextures(1, &id2);

	gl->Flush();
	gl->Finish();
	thread1latch.countDown();

	free_context(gl);
}


int main() {
	std::cout << "Hello" << std::endl;
	
	glfwInit();

	glfwDefaultWindowHints();
	glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
	glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);

	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);


	GLFWwindow* window = glfwCreateWindow(400,400,"test",nullptr,nullptr);
	GLFWwindow* secondary = glfwCreateWindow(1,1,"unused",nullptr,window);

	glfwGetFramebufferSize(window, &fbw,&fbh);
	glfwSetFramebufferSizeCallback(window,framebuffer_size_callback);

	mgl = create_context(window);
	glfwShowWindow(window);


	std::thread th (thread1, secondary);

	thread1latch.await();

	//glfwMakeContextCurrent(glfwGetCurrentContext());

	while (!glfwWindowShouldClose(window)) {
		glfwWaitEventsTimeout(0.1);
		glfwPollEvents();

		render(window, mgl);
	}

	th.join();

	free_context(mgl);

	glfwDestroyWindow(secondary);
	glfwDestroyWindow(window);

	glfwTerminate();
}

void render(GLFWwindow* window,GladGLContext* gl) {
	gl->Viewport(0, 0, fbw, fbh);
	gl->ClearColor(1, 0, 0, 1);
	gl->Clear(GL_COLOR_BUFFER_BIT);
	glfwSwapBuffers(window);
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	fbw = width;
	fbh = height;
	render(window, mgl);
}

void free_context(GladGLContext* gl) {
	free(gl);
	glfwMakeContextCurrent(NULL);
}

GladGLContext* create_context(GLFWwindow* window) {
	glfwMakeContextCurrent(window);
	GladGLContext* context = (GladGLContext*) malloc(sizeof(GladGLContext));
	if (!context)return NULL;
	int version = gladLoadGLContext(context, glfwGetProcAddress);
	std::cout << "Loaded OpenGL " << GLAD_VERSION_MAJOR(version) << "." << GLAD_VERSION_MINOR(version) << std::endl;
	return context;
}

I never set the framebuffersize callback for the secondary window, glfwSwapBuffers never gets called for the second window.

Wanted to remind again, that if you do not specify the version hints (glfwWindowHint(GLFW_CONTEXT_VERSION...), this problem is not present. So I’m gonna ask: Is this a driver problem? If yes, I’d stop bothering you

This could be a driver bug, or a bug in your code which results in undefined behaviour. I don’t have time myself to run and debug the code so it’s difficult to tell. It’s highly unlikely to be a bug in GLFW if it is caused by setting the context hint as the only thing GLFW does is pass it to the driver.