Center the window on screen upon creation

The Partridge Family were neither partridges nor a family. Discuss.
Post Reply
albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Center the window on screen upon creation

Post by albinopapa » May 7th, 2020, 9:31 pm

The chili framework defaults the initial starting position at like ( 300, 300 ) and a default size of ( 800, 600 ).

This puts the window from 300 to 1100 on the X axis and 300 to 900 on the Y axis.

This works fine for resolutions of 1920x1080, but if you have scaling above 100% ( on Windows 8+ ) or a resolution of 1280x720 the window is off the screen near the bottom.

Here's an easy way to create the window in the center of the screen no matter the resolution or scaling.

Code: Select all

	RECT desktop_rect{};
	auto desktop_handle = GetDesktopWindow();
	GetWindowRect( desktop_handle, &desktop_rect );

	// Setup the window as if in the top left corner just to get the adjusted dimensions
	auto wr = RECT{ 0, 0, Graphics::ScreenWidth, Graphics::ScreenHeight };
	AdjustWindowRect( &wr, WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU, FALSE );

	// Now calculate the total width and height of the window
	const auto width = wr.right - wr.left;
	const auto height = wr.bottom - wr.top;
	
	// Calculate the x and y position of the window
	const auto x = ( desktop_rect.right - desktop_rect.left - width ) / 2;
	const auto y = ( desktop_rect.bottom - desktop_rect.top - height ) / 2;
	
	// Create the window with the adjusted X, Y, Width and Height
	hWnd = CreateWindow( wndClassName,L"Main Window Title",
		WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,
		x, y, width, height,
		nullptr,nullptr,hInst,this );
	
This approach should work for windows created with the WS_* flags as is in the chili framework as well as WS_POPUP which doesn't have the borders or title bar.

Anyway, figured I'd share since so many different projects get uploaded here and some have specific resolutions they are trying to use.

NOTE: If you want a 1920x1080 drawing area, you are using the WS_POPUP style instead of WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU. Calling AdjustWindowRect with the WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU styles will cause the RECT to be resized to accommodate the borders and title bard at top. JUST MAKE SURE YOU HAVE A WAY TO EXIT THE PROGRAM SINCE YOU WON'T HAVE ACCESS TO THE RED 'X' OR SYSTEM MENU.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 8th, 2020, 4:27 pm

I've been avoiding this part of Win32/DX programming for a while now, but have finally decided to tackle it. Resizing the window without causing the graphics to stretch. Right now this will only affect the 2D frameworks, I don't recall if chili covers this in the 3D frameworks ( software or hardware ) or not.

First, off the window style chili has chosen prevents the user from being able to click and drag the borders of the window to allow for resizing with the mouse, as well as maximizing the window. This is actually a good thing since there is no code in the framework to handle a resizing event. So here is some code for resizing the window. This code keeps the window centered and expands or shrinks it to whatever the width and height parameters are which I'm using here as the client area I want.

Outline of steps needed to make this work in the Chili framework.
  1. Modify the Graphics class to be movable.
  2. Move Graphics gfx from Game to MainWindow.
  3. Make the Graphics object a Graphics& in Game and initialize it with wnd.gfx.
  4. Change the screen width and height parameters in Graphics from constexpr since we need to be able to change them.
  5. Make a function in MainWindow to be able to change the size of the window programmatically.
  6. Add a function in Graphics for handling resizing the width and height of our back buffers when the window size changes.
  7. Add a case to handle the WM_SIZE message when the window size changes.
The steps are broken up into different posts.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 8th, 2020, 4:27 pm

Modify the Graphics class to be movable.
This is unfortunately unavoidable with the approach I have taken. There are other ways that this could be handled, but hey it's good practice on move semantics. We'll want a move constructor and move assignment operator in our Graphics class. All irrelevant parts of the framework have been removed for readability.

Code: Select all

class Graphics{
public:
	Graphics() = default;
	Graphics( class HWNDKey& key );
	Graphics( const Graphics& ) = delete;
	Graphics( Graphics&& other )noexcept;

	Graphics& operator=( const Graphics& ) = delete;
	Graphics& operator=( Graphics&& other )noexcept;

	~Graphics();
private:
	Microsoft::WRL::ComPtr<IDXGISwapChain>				pSwapChain;
	Microsoft::WRL::ComPtr<ID3D11Device>				pDevice;
	Microsoft::WRL::ComPtr<ID3D11DeviceContext>			pImmediateContext;
	Microsoft::WRL::ComPtr<ID3D11RenderTargetView>		pRenderTargetView;
	Microsoft::WRL::ComPtr<ID3D11Texture2D>				pSysBufferTexture;
	Microsoft::WRL::ComPtr<ID3D11ShaderResourceView>	pSysBufferTextureView;
	Microsoft::WRL::ComPtr<ID3D11PixelShader>			pPixelShader;
	Microsoft::WRL::ComPtr<ID3D11VertexShader>			pVertexShader;
	Microsoft::WRL::ComPtr<ID3D11Buffer>				pVertexBuffer;
	Microsoft::WRL::ComPtr<ID3D11InputLayout>			pInputLayout;
	Microsoft::WRL::ComPtr<ID3D11SamplerState>			pSamplerState;
	D3D11_MAPPED_SUBRESOURCE							mappedSysBufferTexture;
	Color*												pSysBuffer = nullptr;
public:
	static constexpr int ScreenWidth = 800;
	static constexpr int ScreenHeight = 600;
};
All of these members need to be moved and luckily, the ComPtr<...> stuff is like a shared_ptr<...>, but for the Win32 COM objects like Direct3D stuff so it has move semantics built in.

Here's the code for the move constructor...

Code: Select all

Graphics::Graphics( Graphics&& other ) noexcept
{
	*this = std::move( other );
}
Yep, I took the easy way out, by forwarding the responsibility to the move assignment operator, probably not the most efficient, but you shouldn't be moving this object around a lot anyway.

Here's the move assignment operator code

Code: Select all

Graphics& Graphics::operator=( Graphics&& other ) noexcept
{
	if( this != std::addressof( other ) )
	{
		pSwapChain				= std::move(other.pSwapChain);
		pDevice					= std::move(other.pDevice);
		pImmediateContext		= std::move(other.pImmediateContext);
		pRenderTargetView		= std::move(other.pRenderTargetView);
		pSysBufferTexture		= std::move(other.pSysBufferTexture);
		pSysBufferTextureView	= std::move(other.pSysBufferTextureView);
		pPixelShader			= std::move(other.pPixelShader);
		pVertexShader			= std::move(other.pVertexShader);
		pVertexBuffer			= std::move(other.pVertexBuffer);
		pInputLayout			= std::move(other.pInputLayout);
		pSamplerState			= std::move(other.pSamplerState);
		mappedSysBufferTexture	= std::move(other.mappedSysBufferTexture);

		if( pSysBuffer ) _aligned_free( pSysBuffer );			
		pSysBuffer				= other.pSysBuffer;

		other.pSysBuffer = nullptr;
	}

	return *this;
}
As stated, the ComPtr members have move semantics built in, so nothing to do but to forward their moves to their own move assignment operators. However, the pSysBuffer is a raw pointer which means we must handle it ourselves. We must first free the old pSysBuffer if it exists, I'm not sure if _aligned_free checks if the pointer is null, so I check anyway. Then copy the buffer and set the other's buffer to nullptr so the buffer doesn't get freed after the move.

That's it for making Graphics movable. I think the only thing not covered is ( this != std::addressof( other ) ). The std::addressof function is syntactically the same as taking the address of using &, so I could have written: ( this != &other ), but there are cases where the class has the operator& overloaded and doing &other would have ended up calling that overload instead of getting the address of. The std::addressof function bypasses this operator& overload and returns the actual address. While Graphics doesn't have that operator overloaded, I just use std::address of instead of & to stay in the habbit.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 8th, 2020, 4:39 pm

Move Graphics gfx from Game to MainWindow

Code: Select all

class MainWindow : public HWNDKey{
public:
	// irrelevant code removed for readability
	Graphics& GetGraphics()const{ return gfx; }
private:
	Graphics gfx;
	// irrelevant code removed for readability
};
Make the Graphics object a Graphics& in Game and initialize it with wnd.gfx.
Game.h

Code: Select all

class Game
{
public:
	// irrelevant code remove

private:
	Graphics& gfx;
};
Game.cpp

Code: Select all

Game::Game( MainWindow& wnd ) 
	: 
	wnd( wnd ), 
	gfx( wnd.GetGraphics() ) 
{}
Hopefully the code is pretty straight forward. Keeping a reference to the Graphcis object in Game makes it just as convenient as having the object itself in Game.

Change the screen width and height parameters in Graphics from constexpr since we need to be able to change them.
In the Graphics class, the ScreenWidth and ScreenHeight are constexpr meaning they can't be changed during runtime. We need to change them, but we need to keep them easily accessible. We can just make them static and with C++17 enabled, we can also inline initialized them.

Code: Select all

class Graphics
{
public:
	inline static int ScreenWidth = 1024;
	inline static int ScreenHeight = 768;
};
Without the inline keyword, you'd have to declare them here and define them in the Graphics.cpp file. Now they are accessible the same as before, but now they are able to be changed during runtime.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 8th, 2020, 4:42 pm

Make a function in MainWindow to be able to change the size of the window programmatically.
We want to be able to change the size of the window so we need a function in the MainWindow class to handle the details.

Code: Select all

void MainWindow::SetDimensions( int width, int height, int style )
{
	// Setup the window as if in the top left corner of the screen just to get the adjusted dimensions
	auto wr = RECT{ 0, 0, width, height };
	AdjustWindowRect( &wr, style, FALSE );

	// Now calculate the total width and height of the window
	width = wr.right - wr.left;
	height = wr.bottom - wr.top;

	// Get desktop window size to use for centering calculations
	RECT desktop_rect{};
	auto desktop_handle = GetDesktopWindow();
	GetWindowRect( desktop_handle, &desktop_rect );

	// Calculate the x and y position of the window
	const auto x = ( desktop_rect.right - desktop_rect.left - width ) / 2;
	const auto y = ( desktop_rect.bottom - desktop_rect.top - height ) / 2;

	// Use SetWindowPos to set the Z-order of the window, the X/Y position and the size
	SetWindowPos(
		hWnd,
		nullptr,
		x,
		y,
		width,
		height,
		SWP_NOZORDER
	);
}
The SetWindowPos function is a bit of a strange function to me.
  1. The first parameter is the handle to the window being resized.
  2. The second parameter is a handle to the window you want the current window to be on top of. This could be used for bringing a window to the foreground, but there are easier ways.
  3. The third and fourth are the new position for the current window. If you weren't changing the position at all, then you could use 0 for both.
  4. The fifth and sixth are the new size. If you were moving the window, but not resizing, then you could use 0 for both.
  5. The seventh and final parameter is a set of flags telling Windows what you DON'T want changed. The most relevant being NOMOVE, NORESIZE and NOZORDER. You can or '|' them together so to change the size but not the X/Y position or Z order of the window, you'd use NOMOVE | NOZORDER and use nullptr, 0, 0 as the second, third and fourth parameters.
Say you had a game that the user could change the resolution in-game. Maybe when the user presses Alt+Return it goes from a 800x600 bordered window like the chili framework default to a fullscreen borderless window and back. This is the first step. Calling this function will send a WM_SIZE message to the message loop allowing you to handle any further changes that I'll go over in a bit.

One thing to note here is SetWindowPos is expecting a size of the window and not just the client area or drawable region of the window. This is why I don't pass the width and height directly to SetWindowPos, you first need to adjust for the border and title bar if present. The style parameter is there to tell AdjustWindowRect() what features the window will need. So if you pass in WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU, the AdjustWindowRect() function will adjust the size to accommodate the presence of a border and title bar ( the presence of WS_CAPTION alone will actually adjust the window size for both as well ). If you were to pass in WS_POPUP, the AdjustWindowRect returns the rect unchanged since there are no borders or titlebar associated with that Window Style.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 8th, 2020, 7:35 pm

Add a function in Graphics for handling resizing the width and height of our back buffers when the window size changes.

This is the part that I've been avoiding, handling the resizing of the back buffer and all related resources.

First, let's call our function OnResize, the naming is kind of an idiom for event handlers.

Code: Select all

class Graphics
{
public:
	void OnResize( int width, int height );
};

Code: Select all

void Graphics::OnResize( int width, int height )
{
	if( !( ( width == ScreenWidth ) && ( height == ScreenHeight ) ) )
	{
		pImmediateContext->OMSetRenderTargets( 0, nullptr, nullptr );
		pRenderTargetView = Microsoft::WRL::ComPtr<ID3D11RenderTargetView>{};
		pSysBufferTextureView = Microsoft::WRL::ComPtr<ID3D11ShaderResourceView>{};
		pSysBufferTexture = Microsoft::WRL::ComPtr<ID3D11Texture2D>{};

		if( auto hr = pSwapChain->ResizeBuffers(
			0u,
			0u,
			0u,
			DXGI_FORMAT_UNKNOWN,
			0
			); FAILED( hr ) )
		{
			throw std::system_error( hr, std::system_category(), "Failed to resize buffers" );
		}

		ScreenWidth = width;
		ScreenHeight = height;

		// get handle to backbuffer
		ComPtr<ID3D11Resource> pBackBuffer;
		if( auto hr = pSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&pBackBuffer );
			FAILED( hr ) )
			throw GFX_EXCEPTION( hr, L"Getting back buffer" );

		// create a view on backbuffer that we can render to
		if( auto hr = pDevice->CreateRenderTargetView( pBackBuffer.Get(), nullptr, &pRenderTargetView );
			FAILED( hr ) )
			throw GFX_EXCEPTION( hr, L"Creating render target view on backbuffer" );

		// set backbuffer as the render target using created view
		pImmediateContext->OMSetRenderTargets( 1, pRenderTargetView.GetAddressOf(), nullptr );

		auto* mem = _aligned_malloc( width * height * sizeof( Color ), 16 );
		Color* temp = new( mem ) Color{};
		_aligned_free( pSysBuffer );
		pSysBuffer = temp;

		// create texture for cpu render target
		D3D11_TEXTURE2D_DESC sysTexDesc;
		sysTexDesc.Width = ScreenWidth;
		sysTexDesc.Height = ScreenHeight;
		sysTexDesc.MipLevels = 1;
		sysTexDesc.ArraySize = 1;
		sysTexDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
		sysTexDesc.SampleDesc.Count = 1;
		sysTexDesc.SampleDesc.Quality = 0;
		sysTexDesc.Usage = D3D11_USAGE_DYNAMIC;
		sysTexDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
		sysTexDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
		sysTexDesc.MiscFlags = 0;

		// create the texture
		if( auto hr = pDevice->CreateTexture2D( &sysTexDesc, nullptr, &pSysBufferTexture );
			FAILED( hr ) )
			throw GFX_EXCEPTION( hr, L"Creating sysbuffer texture" );

		D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
		srvDesc.Format = sysTexDesc.Format;
		srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
		srvDesc.Texture2D.MipLevels = 1;

		// create the resource view on the texture
		if( auto hr = pDevice->CreateShaderResourceView(
			pSysBufferTexture.Get(),
			&srvDesc,
			&pSysBufferTextureView );
			FAILED( hr ) )
			throw GFX_EXCEPTION( hr, L"Creating view on sysBuffer texture" );
	}
}
There's a lot to go over here so I'll try to break it all down.

Code: Select all

	if( ( width == ScreenWidth ) && ( height == ScreenHeight ) )return;
Since we will need to free and reallocate memory, we probably don't want to do that if the size hasn't changed, though this function should only ever be called when the size does change it doesn't hurt to err on the safe side.

Code: Select all

		pImmediateContext->OMSetRenderTargets( 0, nullptr, nullptr );
		pRenderTargetView = Microsoft::WRL::ComPtr<ID3D11RenderTargetView>{};
		pSysBufferTextureView = Microsoft::WRL::ComPtr<ID3D11ShaderResourceView>{};
		pSysBufferTexture = Microsoft::WRL::ComPtr<ID3D11Texture2D>{};
According the MSDN, IDXGISwapChain::ResizeBuffers requires all render targets associated with the back buffer including their render target views need to be unreferenced and unbound. Since the chili framework has only one render target and one associated render target view, we can just assign an uninitialized copy of the each which should release the interfaces and set the reference counts to 0. We also need to unbind the render target view from the pipeline by passing nullptr to OMSetRenderTargets.

Code: Select all

	if( auto hr = pSwapChain->ResizeBuffers(
		0u,
		0u,
		0u,
		DXGI_FORMAT_UNKNOWN,
		0
		); FAILED( hr ) )
	{
		throw std::system_error( hr, std::system_category(), "Failed to resize buffers" );
	}
According to the documentation, passing all 0's to this function will reuse the DXGI_FORMAT from the old buffer and get the new size from the client area of the window associated with the IDXGISwapChain object. From my testing, this function can fail if there are still references to the associated render target and view, but this code seems to work for me so far.

Code: Select all

	ScreenWidth = width;
	ScreenHeight = height;

	// get handle to backbuffer
	ComPtr<ID3D11Resource> pBackBuffer;
	if( auto hr = pSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&pBackBuffer );
		FAILED( hr ) )
		throw GFX_EXCEPTION( hr, L"Getting back buffer" );

	// create a view on backbuffer that we can render to
	if( auto hr = pDevice->CreateRenderTargetView( pBackBuffer.Get(), nullptr, &pRenderTargetView );
		FAILED( hr ) )
		throw GFX_EXCEPTION( hr, L"Creating render target view on backbuffer" );

	// set backbuffer as the render target using created view
	pImmediateContext->OMSetRenderTargets( 1, pRenderTargetView.GetAddressOf(), nullptr );
The width and height passed to us is the width and height of the drawable area or client area of the window, so we can assign those the the ScreenWidth and ScreenHeight directly.

Since we released the render target ( back buffer ) and view, they need to be recreated.

Code: Select all

	if( auto* mem = _aligned_malloc( width * height * sizeof( Color ), 16 ); mem != nullptr )
	{
		Color* temp = new( mem ) Color{};
		_aligned_free( pSysBuffer );
		pSysBuffer = temp;
	}
	else
	{
		throw std::bad_alloc();
	}
This code may not be familiar to some if not most. Just recently I learned that it is undefined behavior to cast the void* pointer returned from allocating functions. You must initialize the memory with placement new. To use placement new, you have to #include <new>. Also, you'll notice that I allocate to a temp pointer. This is done before freeing the pSysBuffer in case the allocation fails, the previous buffer is still usable. Once sure the allocation was successful, we can deallocate the current buffer and assign the temp buffer pointer to the old buffer pointer.

This should have also been done with the back buffer and render target stuff, but I haven't implemented that part as I'm not sure if I would need to create a second swap chain to get a new back buffer or if doing it this way is enough as I don't believe the back buffer is affected if the function fails.

Code: Select all

	// create texture for cpu render target
	D3D11_TEXTURE2D_DESC sysTexDesc;
	sysTexDesc.Width = ScreenWidth;
	sysTexDesc.Height = ScreenHeight;
	sysTexDesc.MipLevels = 1;
	sysTexDesc.ArraySize = 1;
	sysTexDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
	sysTexDesc.SampleDesc.Count = 1;
	sysTexDesc.SampleDesc.Quality = 0;
	sysTexDesc.Usage = D3D11_USAGE_DYNAMIC;
	sysTexDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
	sysTexDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	sysTexDesc.MiscFlags = 0;

	// create the texture
	if( auto hr = pDevice->CreateTexture2D( &sysTexDesc, nullptr, &pSysBufferTexture );
		FAILED( hr ) )
		throw GFX_EXCEPTION( hr, L"Creating sysbuffer texture" );

	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
	srvDesc.Format = sysTexDesc.Format;
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
	srvDesc.Texture2D.MipLevels = 1;

	// create the resource view on the texture
	if( auto hr = pDevice->CreateShaderResourceView(
		pSysBufferTexture.Get(),
		&srvDesc,
		&pSysBufferTextureView );
		FAILED( hr ) )
		throw GFX_EXCEPTION( hr, L"Creating view on sysBuffer texture" );
The last part is just recreating the resources for the buffer texture and shader resource view used to copy the pSysBuffer to during Graphics::EndFrame().
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 8th, 2020, 7:48 pm

Add a case to handle the WM_SIZE message when the window size changes.

And finally the last part, handling the WM_SIZE message. This part is kind of interesting, but first the code.

Code: Select all

// Place this line at the top of MainWindow.cpp or as a member of the MainWindow class.
// Explanation to follow
std::exception_ptr resize_error = nullptr;

LRESULT MainWindow::HandleMsg( HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam )
{
	switch( msg ){
		case WM_DESTROY:
			PostQuitMessage( 0 );
			break;
		case WM_SIZE:
		{
			try
			{
				gfx.OnResize( LOWORD( lParam ), HIWORD( lParam ) );
			}
			catch( ... )
			{
				resize_error = std::current_exception();
			}
			break;
		}
	}
	return DefWindowProc( hWnd,msg,wParam,lParam );
}
First off, the Windows API doesn't handle exceptions or doesn't propagate exceptions because the C language doesn't have exceptions. So we can't just let the Graphics::OnResize() function throw from inside the MainWindow::HandleMsg() message handler. Luckily, C++ has a work around that I think was originally meant for threads to communicate an exception to the main thread without causing the program to just straight up terminate. We can use it here to allow exceptions from within this C callback. So first we try the OnResize() function and if an error is thrown, the exception is immediately caught and stored in the std::exception_ptr object. As my comment/note states, you can either make it global to the MainWindow.cpp file like I did or add it as a member of MainWindow since this HandleMsg() function is a member of MainWindow it would have access to it. The reason we need to have it global or make a member of MainWindow is because we still need it in one more place outside of the message handler callback function.

Code: Select all

bool MainWindow::ProcessMessage() 
{
	MSG msg{};
	while( PeekMessage( &msg,nullptr,0,0,PM_REMOVE ) )
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );

		if( resize_error )
			std::rethrow_exception( resize_error );

		if( msg.message == WM_QUIT ){
			return false;
		}
	}
	return true;
}
Here in the MainWindow::ProcessMessage() function, we can check that our resize_error has a value and if so, rethrow by calling std::rethrow_exception() using our resize_error exception_ptr. This way chili's main try/catch blocks can handle the messages like normal.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 8th, 2020, 8:06 pm

And that's it. Quite a bit to take in perhaps, but I found this pretty educational. This is the first time I have come across or needed std::exception_ptr and std::rethrow_exception. I have never had to use the SetWindowPos() function before nor the IDXGISwapChain::ResizeBuffers() function.

I'll be testing this out and add notes here if anything changes or needs modifying.

There is another topic I'd like to tackle. Microsoft is trying to get away from bitblt operations in favor of buffer flipping. Currently, the method used in the Chili framework and I think even in the Hardware 3D framework ( though I haven't checked ) is to use DXGI_SWAP_EFFECT_DISCARD. This has been the de facto method for doing full-screen applications as it provides the best performance for full-screen applications. This is because once the pixels have been presented to the screen, the buffer is discarded and a new buffer can be created and used for the next frame before the previous buffer is done drawing. Once it is done, the contents of the back-buffer is copied to the front-buffer and the cycle repeats. This is the bitblt method. The flip model allows the swap chain to just swap buffers instead of copying pixels from one buffer to the next. According to the documentation, starting in Windows 10 fall creators updated, there is a new flip model that makes a full-screen border-less window supposedly as efficient as a dedicated full-screen application, with a few more debugging features.

This is what I want to work on next. Get a full-screen border-less window open and be able to switch between it and a regular lower res window. This by itself shouldn't be too much different, but if I'm understanding the documentation correctly, the render target needs to be updated each frame by getting the back buffer each frame and setting it as the current render target. When I get around to it, I'll probably start a new thread covering that bit of information.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Center the window on screen upon creation

Post by albinopapa » May 10th, 2020, 8:46 pm

And here's the first edit:

After posting I got a little side tracked and forgot to test out the changes. Well, I was a bit surprised when I did go back and test, and things were being drawn offscreen and not crashing. So I did some debugging and found a few things I missed.

First the change to the SetDimensions() function in MainWindow. Now instead of resizing using the SetWindowPos() function, we are just going to move the window so that it is still centered after the resize.

Code: Select all

void MainWindow::SetDimensions( int width, int height, int style )
{
	RECT desktop_rect{};
	auto desktop_handle = GetDesktopWindow();
	GetWindowRect( desktop_handle, &desktop_rect );

	// Setup the window as if in the top left corner just to get the adjusted dimensions
	auto wr = RECT{ 0, 0, width, height };
	AdjustWindowRect( &wr, style, FALSE );

	// Now calculate the total width and height of the window
	const auto dt_width = desktop_rect.right - desktop_rect.left;
	const auto dt_height = desktop_rect.bottom - desktop_rect.top;
	const auto w = wr.right - wr.left;
	const auto h = wr.bottom - wr.top;

	// Calculate the x and y position of the window
	const auto x = ( dt_width - w ) / 2;
	const auto y = ( dt_height - h ) / 2;
	SetWindowPos( hWnd, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER );
	gfx.OnResize( width, height );
}
There is a function called ResizeTarget() that can resize the window for us.

Code: Select all

void Graphics::OnResize( int width, int height )
{
	const auto mode_desc = DXGI_MODE_DESC{
		UINT( width ),
		UINT( height ),
		{ 60,1 },
		DXGI_FORMAT_B8G8R8A8_UNORM,
		DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED,
		DXGI_MODE_SCALING_UNSPECIFIED
	};

	if( auto hr = pSwapChain->ResizeTarget( &mode_desc ); FAILED( hr ) )
	{
		throw std::system_error( hr, std::system_category(), "Failed to resize render target" );
	}
	// Most of the rest of the code is the same as before.
And here's the culprit that I missed and was allowing things to be draw off screen. I forgot about correcting the viewport which is how the algorithm decides what's offscreen or not as well as where to draw them.

Code: Select all

	// set viewport dimensions
	D3D11_VIEWPORT vp;
	vp.Width = float( width );
	vp.Height = float( height );
	vp.MinDepth = 0.0f;
	vp.MaxDepth = 1.0f;
	vp.TopLeftX = 0.0f;
	vp.TopLeftY = 0.0f;
	pImmediateContext->RSSetViewports( 1, &vp );
What that means is that we don't need to handle the WM_SIZE message since we aren't resizing the window ourselves as well as being able to just call the gfx.OnResize() function manually as well. This also means we wouldn't need to delay our exception handling since we aren't trying to get around the exception being thrown from a C callback function.

So there you have it. Resizing and centering the window using both the Win32 APIs and Direct3D APIs.

Now, on to the other project I mentioned.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

Post Reply