Sweet Snippets - Rendering Web Pages to Texture using Awesomium and Direct3D

Published December 30, 2014
Advertisement
With our familiarity with Direct2D now maximized, it's time to move on to other methods of rendering UI and text! At this juncture you're now familiar with Direct2D and it's power (well, maybe not. But let's pretend you are!). However, what if we wanted to render a UI to the screen? In this entry we'll take a look at Awesomium, a Chromium based HTML UI engine.

Introduction


Screenshot%202014-12-29%2021.04.24.png



For rendering UI to the screen you have a few options:

  1. you can go the Direct2D route, drawing images to a buffer and then drawing those buffers to the screen, or drawing directly to the back buffer.
  2. You can render quads to the screen placed where your buttons will be.
  3. You can use some form of a UI library to assist you in drawing such things!

We're going to go for option 3. Now, the framework we're going to be using is called Awesomium. It is not great, but it will do what we need. Some of the problems with it includes a lack of x64 support, some weird behaviors relating to memory management, and some API quirks that are rather annoying. Another option, although a paid for one, is Coherent. However, that aside, Awesomium is fairly easy to get up and running in no time at all. It is based off of Chromium, and hence uses web-kit.

Initializing Awesomium


Initialization is a fairly trivial process. However, the pointer you get out of it needs to be made available to everyone who is going to be using Awesomium. Furthermore, Awesomium doesn't handle threading very well, as such, you should only access it from a single thread. Ever.int main() { Awesomium::WebCore * webCore = Awesomium::WebCore::Initialize(Awesomium::WebConfig()); { MainWindow window(webCore); window.Run(); } Awesomium::WebCore::Shutdown();}
As you can see, it's quite trivial.

Loading a Web Page


Our next step will be to load up some HTML and get it rendering. What better to render than the previous post, of course!m_view.reset(m_webCore->CreateWebView(rect.right, rect.bottom, nullptr, Awesomium::kWebViewType_Offscreen));Awesomium::WebURL url(Awesomium::WSLit("http://www.gamedev.net/blog/32/entry-2260630-sweet-snippets-more-text-rendering-with-directwritedirect2d-and-direct3d11/"));m_view->LoadURL(url);
Now we're getting into one of those quirks. Awesomium has its own string class, and so you need to use the WSLit function to instantiate an instance of it. Additionally, each web view has a size associated with it, which means that any time we change our texture size we'll need to recreate the web view. We tell it to use an off screen view as we actually want to use this as a texture.

Rendering the Page


When the page is done rendering we can get the surface associated with it, and then using UpdateSubresource upload that data to our Direct3D texture.if (m_view->IsLoading()) { m_isLoading = true; m_webCore->Update();}else if (m_isLoading) { m_isLoading = false; m_webCore->Update(); Awesomium::BitmapSurface * surface = static_cast(m_view->surface()); context->UpdateSubresource(m_texture, 0, nullptr, surface->buffer(), surface->width() * 4, 0);}
We keep track of if it was previously loading, so as to note keep updating the texture every time we go through the update loop. After this we simply render a full screen triangle with the updated texture bound as a shader resource:context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr);context->IASetIndexBuffer(nullptr, (DXGI_FORMAT)0, 0);context->IASetInputLayout(nullptr);context->VSSetShader(m_vertexShader, nullptr, 0);context->PSSetShader(m_pixelShader, nullptr, 0);context->PSSetSamplers(0, 1, &m_sampler.p);context->PSSetShaderResources(0, 1, &m_textureView.p);context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);context->Draw(3, 0);
This snippet uses a convenient trick of using the vertex ID to generate a full screen triangle (with texture coordinates) without the need of a vertex buffer nor index buffer. See this presentation for more information.

Other Things of Note


The only additional thing to note is our handling of WM_SIZE and WM_EXITSIZEMOVE. Due to the fact that Awesomium creates a new process for each view (although you can avoid this by using child views), resizing the window becomes a chore. As such, we ignore all WM_SIZE messages except SIZE_MAXIMIZED ones, or if the window was previously maximized. This allows us to only recreate the view when the window is maximized, restored from a maximized state, or resizing has finished.LRESULT OnSize(unsigned message, WPARAM wParam, LPARAM lParam, BOOL & handled) { if (wParam == SIZE_MAXIMIZED) { m_isMaximized = true; return OnSizeFinish(message, wParam, lParam, handled); } else { if (m_isMaximized) { m_isMaximized = false; return OnSizeFinish(message, wParam, lParam, handled); } } return 0;}
When this is the case we release our texture, shader view, and web-view, then we recreate them at the new size. Finally we issue a new load request to the same URL as before. When this completes the update loop will properly update the texture with the new data.

Full Sample

#define NOMINMAX#include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "d3d11.lib")#pragma comment(lib, "awesomium.lib")#include #include #include #include #include #include #include #ifdef UNICODEtypedef wchar_t tchar;typedef std::wstring tstring;#elsetypedef char tchar;typedef std::string tstring;#endifvoid ThrowIfFailed(HRESULT result, std::string const & text) { if (FAILED(result)) throw std::runtime_error(text + "");}class RenderTarget {public: RenderTarget(ID3D11Texture2D * texture, bool hasDepthBuffer) : m_texture(texture) { CComPtr device; texture->GetDevice(&device); auto result = device->CreateRenderTargetView(m_texture, nullptr, &m_textureRTV); ThrowIfFailed(result, "Failed to create back buffer render target."); m_viewport = CD3D11_VIEWPORT(m_texture, m_textureRTV); result = device->CreateTexture2D(&CD3D11_TEXTURE2D_DESC(DXGI_FORMAT_D32_FLOAT, static_cast(m_viewport.Width), static_cast(m_viewport.Height), 1, 1, D3D11_BIND_DEPTH_STENCIL), nullptr, &m_depthBuffer); ThrowIfFailed(result, "Failed to create depth buffer."); result = device->CreateDepthStencilView(m_depthBuffer, nullptr, &m_depthView); ThrowIfFailed(result, "Failed to create depth buffer render target."); } void Clear(ID3D11DeviceContext * context, float color[4], bool clearDepth = true) { context->ClearRenderTargetView(m_textureRTV, color); if (clearDepth && m_depthView) context->ClearDepthStencilView(m_depthView, D3D11_CLEAR_DEPTH, 1.0f, 0); } void SetTarget(ID3D11DeviceContext * context) { context->OMSetRenderTargets(1, &m_textureRTV.p, m_depthView); context->RSSetViewports(1, &m_viewport); }private: D3D11_VIEWPORT m_viewport; CComPtr m_depthBuffer; CComPtr m_depthView; CComPtr m_texture; CComPtr m_textureRTV;};class GraphicsDevice {public: GraphicsDevice(HWND window, int width, int height) { D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, }; DXGI_SWAP_CHAIN_DESC desc = { { width, height, { 1, 60 }, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED, DXGI_MODE_SCALING_UNSPECIFIED }, { 1, 0 }, DXGI_USAGE_BACK_BUFFER | DXGI_USAGE_RENDER_TARGET_OUTPUT, 1, window, TRUE, DXGI_SWAP_EFFECT_DISCARD, 0 }; auto result = D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_DEBUG | D3D11_CREATE_DEVICE_BGRA_SUPPORT, levels, sizeof(levels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &desc, &m_swapChain, &m_device, &m_featureLevel, &m_context ); ThrowIfFailed(result, "Failed to create D3D11 device."); } void Resize(int width, int height) { if (m_renderTarget) m_renderTarget.reset(); auto result = m_swapChain->ResizeBuffers(1, width, height, DXGI_FORMAT_UNKNOWN, 0); ThrowIfFailed(result, "Failed to resize back buffer."); CComPtr backBuffer; result = m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&backBuffer)); ThrowIfFailed(result, "Failed to retrieve back buffer surface."); m_renderTarget = std::make_unique(backBuffer, true); } void SetAndClearTarget() { static float color[] { 0, 0, 0, 0}; if (!m_renderTarget) return; m_renderTarget->Clear(m_context, color); m_renderTarget->SetTarget(m_context); } void Present() { m_swapChain->Present(0, 0); } ID3D11Device * GetDevice() { return m_device; } ID3D11DeviceContext * GetDeviceContext() { return m_context; }private: D3D_FEATURE_LEVEL m_featureLevel; CComPtr m_device; CComPtr m_context; CComPtr m_swapChain; std::unique_ptr m_renderTarget;};class MainWindow : public CWindowImpl {public: DECLARE_WND_CLASS_EX(ClassName, CS_OWNDC | CS_HREDRAW | CS_VREDRAW, COLOR_BACKGROUND + 1); MainWindow(Awesomium::WebCore * webCore) : m_webCore(webCore), m_view(nullptr, [](Awesomium::WebView * ptr) { ptr->Destroy(); }), m_isMaximized(false) { RECT rect = { 0, 0, 800, 600 }; AdjustWindowRectEx(&rect, GetWndStyle(0), FALSE, GetWndExStyle(0)); Create(nullptr, RECT{ 0, 0, rect.right - rect.left, rect.bottom - rect.top }, WindowName); ShowWindow(SW_SHOW); UpdateWindow(); } void Run() { MSG msg; while (true) { if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break; TranslateMessage(&msg); DispatchMessage(&msg); } else { Update(); } } } void Update() { auto context = m_device->GetDeviceContext(); if (m_view->IsLoading()) { m_isLoading = true; m_webCore->Update(); } else if (m_isLoading) { m_isLoading = false; m_webCore->Update(); Awesomium::BitmapSurface * surface = static_cast(m_view->surface()); context->UpdateSubresource(m_texture, 0, nullptr, surface->buffer(), surface->width() * 4, 0); } m_device->SetAndClearTarget(); context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); context->IASetIndexBuffer(nullptr, (DXGI_FORMAT)0, 0); context->IASetInputLayout(nullptr); context->VSSetShader(m_vertexShader, nullptr, 0); context->PSSetShader(m_pixelShader, nullptr, 0); context->PSSetSamplers(0, 1, &m_sampler.p); context->PSSetShaderResources(0, 1, &m_textureView.p); context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); context->Draw(3, 0); m_device->Present(); }private: BEGIN_MSG_MAP(MainWindow) MESSAGE_HANDLER(WM_DESTROY, [](unsigned messageId, WPARAM wParam, LPARAM lParam, BOOL & handled) { PostQuitMessage(0); return 0; }); MESSAGE_HANDLER(WM_CREATE, OnCreate); MESSAGE_HANDLER(WM_SIZE, OnSize); MESSAGE_HANDLER(WM_EXITSIZEMOVE, OnSizeFinish); END_MSG_MAP()private: LRESULT OnCreate(unsigned message, WPARAM wParam, LPARAM lParam, BOOL & handled) { try { RECT rect; GetClientRect(&rect); m_device = std::make_unique(m_hWnd, rect.right, rect.bottom); tstring filename(MAX_PATH, 0); GetModuleFileName(GetModuleHandle(nullptr), &filename.front(), filename.length()); filename = filename.substr(0, filename.find_last_of('\\')); SetCurrentDirectory(filename.c_str()); std::vector vs(std::istreambuf_iterator(std::ifstream("VertexShader.cso", std::ios_base::in | std::ios_base::binary)), std::istreambuf_iterator()); auto result = m_device->GetDevice()->CreateVertexShader(&vs.front(), vs.size(), nullptr, &m_vertexShader); ThrowIfFailed(result, "Could not create vertex shader."); std::vector ps(std::istreambuf_iterator(std::ifstream("PixelShader.cso", std::ios_base::in | std::ios_base::binary)), std::istreambuf_iterator()); result = m_device->GetDevice()->CreatePixelShader(&ps.front(), ps.size(), nullptr, &m_pixelShader); ThrowIfFailed(result, "Could not create pixel shader."); result = m_device->GetDevice()->CreateSamplerState(&CD3D11_SAMPLER_DESC(CD3D11_DEFAULT()), &m_sampler); ThrowIfFailed(result, "Could not create sampler state."); m_device->GetDevice()->CreateTexture2D(&CD3D11_TEXTURE2D_DESC(DXGI_FORMAT_B8G8R8A8_UNORM, rect.right, rect.bottom, 1, 1), nullptr, &m_texture); ThrowIfFailed(result, "Could not create web texture."); m_device->GetDevice()->CreateShaderResourceView(m_texture, nullptr, &m_textureView); ThrowIfFailed(result, "Could not create shader view."); m_view.reset(m_webCore->CreateWebView(rect.right, rect.bottom, nullptr, Awesomium::kWebViewType_Offscreen)); Awesomium::WebURL url(Awesomium::WSLit("http://www.gamedev.net/blog/32/entry-2260630-sweet-snippets-more-text-rendering-with-directwritedirect2d-and-direct3d11/")); m_view->LoadURL(url); m_device->Resize(rect.right, rect.bottom); } catch (std::runtime_error & ex) { std::cout << ex.what() << std::endl; return -1; } return 0; } LRESULT OnSizeFinish(unsigned message, WPARAM wParam, LPARAM lParam, BOOL & handled) { try { RECT clientRect; GetClientRect(&clientRect); m_device->Resize(clientRect.right, clientRect.bottom); if (m_texture) m_texture.Release(); if (m_textureView) m_textureView.Release(); m_device->GetDevice()->CreateTexture2D(&CD3D11_TEXTURE2D_DESC(DXGI_FORMAT_B8G8R8A8_UNORM, clientRect.right, clientRect.bottom, 1, 1), nullptr, &m_texture); m_device->GetDevice()->CreateShaderResourceView(m_texture, nullptr, &m_textureView); if (m_view->IsLoading()) m_view->Stop(); m_view.reset(m_webCore->CreateWebView(clientRect.right, clientRect.bottom, nullptr, Awesomium::kWebViewType_Offscreen)); Awesomium::WebURL url(Awesomium::WSLit("http://www.gamedev.net/blog/32/entry-2260630-sweet-snippets-more-text-rendering-with-directwritedirect2d-and-direct3d11/")); m_view->LoadURL(url); } catch (std::runtime_error & ex) { std::cout << ex.what() << std::endl; } return 0; } LRESULT OnSize(unsigned message, WPARAM wParam, LPARAM lParam, BOOL & handled) { if (wParam == SIZE_MAXIMIZED) { m_isMaximized = true; return OnSizeFinish(message, wParam, lParam, handled); } else { if (m_isMaximized) { m_isMaximized = false; return OnSizeFinish(message, wParam, lParam, handled); } } return 0; }private: std::unique_ptr m_device; Awesomium::WebCore * m_webCore; std::unique_ptr m_view; CComPtr m_pixelShader; CComPtr m_vertexShader; CComPtr m_sampler; CComPtr m_texture; CComPtr m_textureView; bool m_isLoading; bool m_isMaximized;private: static const tchar * ClassName; static const tchar * WindowName;};const tchar * MainWindow::WindowName = _T("DX Window");const tchar * MainWindow::ClassName = _T("GameWindowClass");int main() { Awesomium::WebCore * webCore = Awesomium::WebCore::Initialize(Awesomium::WebConfig()); { MainWindow window(webCore); window.Run(); } Awesomium::WebCore::Shutdown();}
5 likes 5 comments

Comments

TheItalianJob71

I wanted to use Awesomium as a gui for my project, do you have experiences in such directions ? is it good for a typical gui usage , think of the titanium sdk, with its sleek graphics, and did you notice speed slowing down due to the rendering offscreen and uploading a big texture in realtime on the screen ???

December 30, 2014 09:58 AM
Jason Z

I am really enjoying the sweet snippets series of articles - they are well written, have interesting topics, and will be useful to many people. Great job!

December 30, 2014 01:58 PM
Washu

I wanted to use Awesomium as a gui for my project, do you have experiences in such directions ? is it good for a typical gui usage , think of the titanium sdk, with its sleek graphics, and did you notice speed slowing down due to the rendering offscreen and uploading a big texture in realtime on the screen ???

I'm actually going to handle part of your questions in another snippet. But lets talk a bit about the rendering bit:

Awesomium (and most HTML rendering engines) use another process entirely to do the rendering, then there's some IPC that happens to move the data over to your process (which is done invisibly in the m_webCore->Update() calls).

As for updating a full screen texture: Yes, there is a reasonable amount of lag, but your UI rarely takes up the entire screen, so there are two options:

1. if you're rendering full screen, provide a custom Surface which captures the dirty rects that need upating and only updates those portions of the texture (which is what I'm going to show in the next snippet).

2. Only render to a texture of an appropriate size for the piece of the UI you wish to show. For instance if you have a menu, you might only render to a texture of an appropriate size for the menu (512x512, for instance).

December 30, 2014 07:25 PM
Ovicior

Very well written, even though I only understood about 50% of it.

January 01, 2015 06:32 AM
TriRozhka

Nice tutorial, but where i can find vertex and pixel shader source code? Hope someone helps me.

September 11, 2016 04:13 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement