Why does resizing the window take longer (0.3s) the first time?

When I resize the window for the first time the game freezes for about 0.3 seconds. After that, resizing again happens immediately. In the Visual Studio 13 debugger, though, the first resize takes about 3 seconds. Why could that be? Is it avoidable? (I use OpenGL and Windows)


EDIT: Initially I thought it was 3 seconds without debugging, too, because I hadn't tested that. I can take 3 seconds in the debugger, but if I could avoid the 0.3 seconds for the user, it would be a noticeable improvement.

Edited by Opoiregwfetags on
In which part of code game freezes for 0.3 second? Can you measure different parts to determine in what function this happens?
I measured. In the debugger, the totality of the extra ~3s happens at whatever OpenGL draw call is in the first position of my list of render commands (e.g. glClearColor(), glBegin(GL_TRIANGLES), etc.). I assume the ~0.3s outside the debugger will be attributed to the same thing, but I don't know why the amount of time is different.

EDIT: If the first draw command is a clear, the function that takes up the ~3s is actually glClear(), not glClearColor() even though that one's called before. If the first draw command is a draw rectangle, the function that takes up the ~3s is glBegin(GL_TRIANGLES) (even though I call glDisable(GL_TEXTURE_2D) before it).

Edited by Opoiregwfetags on
You should not rely on debugger for measuring time. MSVC debugger is not good at it. You should put explicit calls to measure time to know what's happening (or use a good profiler).

Anyways - normally this should not happen. I tried it on simple GL code, and could not measure noticeable difference for glClear after window resize.

It's hard to guess what could be wrong. You'll need to share more code - so others we try running it to see if that also happens (meaning something can be fixed in code) or it does not happen, which would mean problem is only on your system - some resident software or OS/driver/GPU issue.
I measured the times with "explicit calls", with printf's, but on the debugger because that's where I have a console. Is that unreliable too?

Anyway, here's some relevant functions. They really are pretty much what I saw on HmH when Casey switched to hardware rendering with OpenGL. I think this will be enough, I just picked the OpenGL functions because it seems to be the cause. I think Windows stuff might also have something to do with it, but that code's very generic and simple, also identical to HmH's... But if you want me to share a simplified version of the whole project to run it, I can try to do that.

This gets called first.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
internal void
Win32InitOpenGL(HDC windowDC){
	PIXELFORMATDESCRIPTOR DesiredPixelFormat = {};
	DesiredPixelFormat.nSize = sizeof(DesiredPixelFormat);
	DesiredPixelFormat.nVersion = 1;
	DesiredPixelFormat.iPixelType = PFD_TYPE_RGBA;
	DesiredPixelFormat.dwFlags = PFD_SUPPORT_OPENGL|PFD_DRAW_TO_WINDOW|PFD_DOUBLEBUFFER;
	DesiredPixelFormat.cColorBits = 32;
	DesiredPixelFormat.cAlphaBits = 8;
	DesiredPixelFormat.iLayerType = PFD_MAIN_PLANE;
	
	int SuggestedPixelFormatIndex = ChoosePixelFormat(windowDC, &DesiredPixelFormat);
	PIXELFORMATDESCRIPTOR SuggestedPixelFormat;
	DescribePixelFormat(windowDC, SuggestedPixelFormatIndex,
						sizeof(SuggestedPixelFormat), &SuggestedPixelFormat);
	SetPixelFormat(windowDC, SuggestedPixelFormatIndex, &SuggestedPixelFormat);

	HGLRC OpenGLRC = wglCreateContext(windowDC);
	if (wglMakeCurrent(windowDC, OpenGLRC)){
		OpenGLInit();

		wglSwapInterval = (wgl_swap_interval_ext *)wglGetProcAddress("wglSwapIntervalEXT");
		if (wglSwapInterval){
			wglSwapInterval(1);
		}
	}else{
		InvalidCodePath;
	}
}

Which calls this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
global GLuint openGlDefaultInternalTextureFormat;
#define GL_FRAMEBUFFER_SRGB	0x8DB9
#define GL_SRGB8_ALPHA8 	0x8C43	
internal void OpenGLInit(){
	opengl_info info = OpenGLGetInfo();

	openGlDefaultInternalTextureFormat = GL_RGBA8;
	if (info.GL_EXT_texture_sRGB)
		openGlDefaultInternalTextureFormat = GL_SRGB8_ALPHA8;

	if (info.GL_EXT_framebuffer_sRGB)
		glEnable(GL_FRAMEBUFFER_SRGB);
}

Which calls this: (this simply gets the available extensions)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct opengl_info{
	char *Vendor;
	char *Renderer;
	char *Version;
	char *ShadingLanguageVersion;
	char *Extensions;

	b32 GL_EXT_texture_sRGB;
	b32 GL_EXT_framebuffer_sRGB;

};
internal opengl_info OpenGLGetInfo(void){
	opengl_info result = {};

	result.Vendor					= (char *)glGetString(GL_VENDOR);
	result.Renderer					= (char *)glGetString(GL_RENDERER);
	result.Version					= (char *)glGetString(GL_VERSION);
	result.ShadingLanguageVersion	= "(none)";

	result.Extensions				= (char *)glGetString(GL_EXTENSIONS);

	char *at = result.Extensions;
	while(*at){
		while(IsWhitespace(*at))
			at++;
		char *end = at;
		while(*end && !IsWhitespace(*end))
			  end++;

		umm count = end - at;

		if		(StringsAreEqual(count, at, "GL_EXT_texture_sRGB"))		{result.GL_EXT_texture_sRGB=true;}
		else if (StringsAreEqual(count, at, "GL_EXT_framebuffer_sRGB")) {result.GL_EXT_framebuffer_sRGB=true;}

		at = end;
	}

	return result;
}


Then every frame we call this:
1
2
3
4
5
6
internal void 
Win32RenderInWindow(HDC deviceContext, game_render_commands *commands,
					int windowWidth, int windowHeight, void *sortMemory){
	SortEntries(commands, sortMemory);
	OpenGLRenderCommands(commands, windowWidth, windowHeight);	    
}

Which calls this:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
inline void OpenGLRenderCommands(game_render_commands *commands, 
								 s32 windowWidth, s32 windowHeight){

	glViewport(0, 0, windowWidth, windowHeight);

	
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //deleteme
    
    glMatrixMode(GL_TEXTURE);
    glLoadIdentity();
	
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glMatrixMode(GL_PROJECTION);
	f32 a = SafeRatio1(2.0f, (f32)commands->width);
	f32 b = SafeRatio1(2.0f, (f32)commands->height);
	f32 proj[] = {
		a, 0, 0, 0,
		0, b, 0, 0,
		0, 0, 1, 0,
		-1.0f, -1.0f, 0, 1
	};
	glLoadMatrixf(proj);

    
    u32 sortEntryCount = commands->pushBufferElementCount;
    render_sort_entry *sortEntries = (render_sort_entry *)
									 (commands->pushBuffer + commands->sortEntryAt);

    render_sort_entry *entry = sortEntries + (sortEntryCount-1);


	 for(u32 i = 0; i < sortEntryCount; i++){
		render_command_header *header = (render_command_header *)
										 (commands->pushBuffer + entry->pushBufferOffset);

		u8 *data = (u8 *)header + sizeof(render_command_header);
		switch(header->type){
		case RenderCommand_Clear:
		{
			render_command_clear *command = (render_command_clear *)data;
			glClearColor(command->color.r, command->color.g, command->color.b, command->color.a);
			
			glClear(GL_COLOR_BUFFER_BIT);
		 } break;
		 
		 case RenderCommand_BitmapRect:
		 {
			render_command_bitmap_rect *command = (render_command_bitmap_rect *)data;
			
			if (command->bitmap->texHandle){
				glBindTexture(GL_TEXTURE_2D, command->bitmap->texHandle);
			}else{
				// Upload and bind texture.
				textureBindCount ++; // Static variable counter
				command->bitmap.texHandle = textureBindCount;
				glBindTexture(GL_TEXTURE_2D, command->bitmap.texHandle);
				
				glTexImage2D(GL_TEXTURE_2D, 0, openGlDefaultInternalTextureFormat,
							command->bitmap.width, command->bitmap.height, 0, GL_RGBA,
							GL_UNSIGNED_BYTE, command->bitmap.mem);
    
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);    
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);    
 				glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
			}
			
			v2 pMin = command->pMin;
			v2 pMax = command->pMax;
			v2 tMin = command->texPMin;
			v2 tMax = command->texPMax;

			glBegin(GL_TRIANGLES);
				glColor4f(command->color.r, command->color.g, command->color.b, command->color.a);
				glTexCoord2f(tMin.x, tMin.y); // Lower triangle
				glVertex2f(  pMin.x, pMin.y);
				glTexCoord2f(tMax.x, tMin.y);
				glVertex2f(  pMax.x, pMin.y);
				glTexCoord2f(tMax.x, tMax.y);
				glVertex2f(  pMax.x, pMax.y);
				glTexCoord2f(tMin.x, tMin.y); // Upper triangle
				glVertex2f(  pMin.x, pMin.y);
				glTexCoord2f(tMax.x, tMax.y);
				glVertex2f(  pMax.x, pMax.y);
				glTexCoord2f(tMin.x, tMax.y);
				glVertex2f(  pMin.x, pMax.y);
			glEnd();
		 } break;
		case RenderCommand_Rectangle:
		 {
			render_command_rectangle *command = (render_command_rectangle *)data;

			glDisable(GL_TEXTURE_2D);
			glBegin(GL_TRIANGLES);
				glColor4f(command->color.r, command->color.g, command->color.b, command->color.a);
				glVertex2f(command->p.x, command->p.y); // Lower triangle
				glVertex2f(command->p.x+command->dim.x, command->p.y);
				glVertex2f(command->p.x+command->dim.x, command->p.y+command->dim.y);
				glVertex2f(command->p.x, command->p.y); // Upper triangle
				glVertex2f(command->p.x+command->dim.x, command->p.y+command->dim.y);
				glVertex2f(command->p.x, command->p.y+command->dim.y);
			glEnd();
			glEnable(GL_TEXTURE_2D);
		 } break;
		/* ... */
		}
	entry--;
	}
}

And then this:
1
2
3
internal void Win32FinishRender(HDC deviceContext){
	SwapBuffers(deviceContext);
}


This is not a breaking problem, so if solving it is too painful, I can accept it, and I could even ship with this.

Edited by Opoiregwfetags on
If you could share a minimal complete reproduction case and build.bat file it would make it easier for us to try it. And leave your profiling code in it.
Ok, here's the project: https://we.tl/t-M8A4U4nNdU

EDIT: Got rid of ~300 LoC for more simplicity.

Edited by Opoiregwfetags on
I can't reproduce the issue on my machine.

The biggest time I get is about 0.001s for the clear and 0.002s for the whole rendering. And it only happens on the first frame, and only on the first frame. That's while running in the debugger (VS2019) on windows 7.

The rest of the time, the clear takes about 0.00002s and the rendering 0.0002. Resizing the window doesn't affect the time at all.

So I'm not sure I can help much, maybe mmozeiko will be able to reproduce the issue, but it might be related to your config. What CPU, GPU, OS are you running. For reference, my config is quite old, i7 860 and Radeon R9 280.
Well, thanks a lot for trying anyway. My OS, CPU, GPU, are Windows 10 Pro, Intel Core i5-8265U @1.60GHz, Intel UHD Graphics 620.
I also cannot reproduce this on my desktop (with nvidia gpu). All times are pretty close to 0.
It might be something on your PC - maybe try updating GPU driver, or check what software you're running in background. Try to close as much as possible, maybe something is affecting program.

One thing I could suggest to try is to call DefWindowProc in WM_SIZE and WM_PAINT messages, and do not call BeginPaint/EndPaint there. But not sure if that'll fix anything.

When you get those 3 second delay in function, try to stop debugger (so it stops inside glClear function) - and examine call stack. Maybe names of functions in callstack will give clue what is happening. You can save minidump (Debug -> Save Dump As...) to share it here, so we can also look at it.

Edited by Mārtiņš Možeiko on
I updated GPU driver. I restarted the computer. I closed other programs and a bunch of random processes I don't even know what they are, that sounded related to graphics. Still the first resize takes about 0.3s outside and 3s inside the debugger.

Calling DefWindowProc in WM_SIZE and WM_PAINT and not calling BeginPaint/EndPaint didn't fix it either.

Stopping the debugger during the 3s bug shows a long list of about 30-50 function calls (the earlier I press Stop Debugging the less function calls I get, but it's not too predictable) in the call stack, from 3 dlls: first "ig9icd64.dll", then "igc64.dll", then "ntdll.dll".


Here's the "minidump" for download (278MB): https://we.tl/t-hfLjRYqW77
You can right click on call stack entries from standard windows dlls like ntdll.dll and kernel32.dll and so on and choose "Load Symbols" - then debugger will download pdb from Microsoft server and show names of functions.

In your call stack it seems those ntdll functions are related to freeing memory - "RtlHeapFree" and similar. There's also some functions related to debug heap - "RtlDebugFreeHeap". debug heap probably is the difference between 0.3 vs 3 seconds. You can disable debug heap by setting _NO_DEBUG_HEAP=1 environment variable. Other than that I have no idea why heap functions take so much time here.
Yeah, with _NO_DEBUG_HEAP=1 I get the same times as without debugger, which, now that I can measure, are usually 0.17-0.22s, and rarely get to .33f. I guess I'll just have to accept that, maybe only a small minority of players will encounter this slight inconvenience. Anyway, thanks a lot again!