The Windows WebView2 is the modern, Chromium-based webview control built in to Windows. We use it for Construct as an option for packaging up web-based games for Windows. It works great as a lightweight alternative to frameworks like NW.js and Electron, which bundle the entire browser engine with the app, and throw in all of node.js to boot.
Steam is of course the biggest store for PC games, and obviously people who publish games will want them to be on Steam. Steam includes a feature called the Steam Overlay which shows some in-game content from Steam while you're playing a game. It looks a bit like this.
The Steam Overlay showing over a game running in Steam. Typically the game is still visible through a semitransparent background.
If you publish a game to Steam, naturally you'd like it to work with Steam features like the Steam Overlay. So I wanted to make sure the Steam Overlay could appear on top of an app using WebView2. How hard can it be? It turns out: very hard. I actually failed to get it to work at all: it appears to be impossible to correctly show the Steam Overlay over WebView2. I wrote this post so at least I got a blog post out of a couple of days otherwise wasted work! The technical details are quite interesting too, and perhaps I could save someone else the trouble. So, this is the blog post I wish I could have read before trying!
How the Steam Overlay works
Documentation is fairly limited on the inner workings of the Steam Overlay, but some Steam documentation states:
Your game does not need to do anything special for the overlay to work, it automatically hooks into any game launched from Steam! ... The overlay supports games that use DirectX 7 - 12, OpenGL, Metal, and Vulkan.
At first glance, it looks like things will work: WebView2 uses the Chromium browser engine, which uses the ANGLE graphics library, which translates OpenGL to Direct3D 11 on Windows (by default - there's lots of different backend combinations).
However running the WebView2 app from Steam, no overlay shows up. It's difficult to know for sure as Steam support haven't been too helpful yet, but I suspect this is due to the multi-process architecture of modern browser engines. The Windows task manager shows the different processes Edge uses nicely.
A list of the different browser processes used by the Microsoft Edge browser, as shown by Task Manager on Windows.
The Chrome developer blog Inside look at modern web browser has more details if you want to know more. However the key part is: browsers use a separate GPU process. All their calls to graphics APIs like DirectX, OpenGL, Metal or Vulkan are now done from a different process to the main app. This is a good architecture: it has built-in multithreading to limit the performance overhead, but also GPU code and drivers can be super complicated; if it fails only the GPU process crashes, and it can potentially recover from that.
The undocumented limitation in the Steam Overlay appears to be that it only supports rendering over graphics rendered from the main app process. It seems the Steam Overlay has quite an intrusive design, hooking in to the actual rendering code the game uses, and inserting extra drawing at the end of the frame to draw the overlay content directly over the very same backbuffer the game is rendering to. And it only knows to do this for the main process.
Browsers and frameworks like Electron and NW.js which are also builds of Chromium can work around this with the command-line flag --in-process-gpu
. This moves all the GPU code to the main process again, and the Steam Overlay works. However it does not work for an app using WebView2. I suspect this is because the process architecture of WebView2 is slightly different. Browsers have a main process and GPU process, and --in-process-gpu
merges them. However WebView2 has an app process, a WebView2 process, and a GPU process; --in-process-gpu
still merges the WebView2 process and GPU process, but you're left with an app process and a WebView2 process. Rendering is still happening in a different process, and Steam is not happy.
I explained all this to Steam support but they've not responded usefully yet. (If anyone from Valve is reading: please look at ticket HT-3BB7-YQY6-N3MQ!) WebView2 doesn't appear to have any way to customize the process model to work around it. So, what to do?
Maybe we can hack it!
So, Steam wants Direct3D surface in the main process, eh? What if we give it one? The app could render a transparent Direct3D surface on top of WebView2, solely so Steam can render its overlay in to it. A neat trick if it can be made to work. Unfortunately, as I mentioned, this cannot actually be made to work! Here's what I tried.
Attempt 1: layered windows
I'm working on Windows with C++ and good old Win32 APIs. Fortunately I have some familiarity with all this, as past versions of Construct were built using these technologies, so I know the right combination of C++, Win32, JavaScript and modern web APIs to understand all the moving parts here. I thought: let's create a child window with support for alpha transparency layered on top of the app. It turns out this is not what the Win32 windowing APIs were ever designed to support. All the stuff to do with HWNDs is based on a 90s design when merely overlapping two windows was pretty novel. Since then lots of new stuff has been bolted on top with almost legendary support for backwards compatibility.
After some time I figured out I needed a child window (WS_CHILD
) which was layered (WS_EX_LAYERED
). It turns out this combination is only supported on Windows 8+, but that's OK for us, as modern browsers only support Windows 10+ now anyway. However the call to CreateWindowEx
failed with error code 0. What does error code 0 mean? It means success. Not very helpful.
After much too long wrestling with Google searches, which seem to return much less useful results than in the past, I eventually came across a note in the Microsoft documentation that says you need an application manifest (a small XML file embedded in the app), which needs the magic line <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
to say "this app knows about Windows 10". Not particularly intuitive and not well documented for Visual Studio either. Anyway once I got that working the CreateWindowEx
call works. But nothing showed up. Further reading indicated that before a layered window appears you must first call either SetLayeredWindowAttributes
or UpdateLayeredWindow
.
I spent hours trying to get this work. My ultimate conclusion is it can't be done. It seems to be the two options are:
SetLayeredWindowAttributes
just lets you set a single transparent color, or an opacity for the entire window. It does not let you use an alpha channel.
UpdateLayeredWindow
lets you set content to be drawn from an HDC. That's a handle to a device context, another truly ancient and creaking part of Windows that is unfortunately not relevant to the task at hand: I need to render a Direct3D surface, which involves swapchains - HDCs are not involved in that.
So I gave up. Too bad. But perhaps there's another way...
Attempt 2: DirectComposition
DirectComposition is a technology also introduced with Windows 8 that allows high-performance layering of surfaces with transparency, and much more. Better still, Direct3D 11 allows rendering to a DirectComposition layer (via CreateSwapChainForComposition
). It sounded like a much better fit for what I was trying to do.
So the approach I came up with was:
- The app shows a WebView2 control normally
- Create a DirectComposition target on top of the window content (helpfully layered on top of WebView2)
- Create a single visual layer with DirectComposition
- Create a Direct3D 11 device, and create a swapchain that renders in to that visual layer
- Fill the background with transparency every frame
This also took a lot of wrestling to get to work right. There were also seemingly undocumented quirks in the Direct3D API as well. For example the AlphaMode
parameter when creating a swapchain appears to support various modes like "unspecified", "premultiplied", "straight" and "ignore"; however "premultiplied" mode never works with layered windows, and all other modes never work with a swapchain - you have to specify "premultiplied" to successfully create a swapchain for composition.
So after finally getting all the right parameters lined up... it showed the Steam Overlay! That validates this approach works in principle. Unfortunately, it doesn't work in practice. The Steam Overlay looks wrong. It's all pale and washed out, like this.
The problem appears to be:
- DirectComposition does alpha blending with premultiplied alpha. 50% transparent white is represented as RGBA (0.5, 0.5, 0.5, 0.5). This is the right choice for blending and compositing.
- The Steam Overlay renders with unpremultiplied (aka straight) alpha. 50% transparent white is represented as RGBA (1, 1, 1, 0.5).
I won't go in to all the details of premultiplied vs. unpremultiplied alpha here. However suffice to say they render transparency in different modes, and so blending works differently.
Unfortunately it seems I'm stuck again. There doesn't appear to be any workaround to this. The Steam Overlay renders directly over the Direct3D 11 backbuffer at the end of a frame and there seems to be no good way to modify the results of its rendering. DirectComposition only supports compositing with premultiplied alpha, because frankly that's the correct way to do alpha blending. The Steam Overlay using unpremultiplied alpha is probably only a problem for me because typically games render to an opaque background, so the way the Steam Overlay handles alpha doesn't matter. It's only when you get in to my situation and you want to layer it over something else where the way the alpha blending is done becomes significant.
Conclusion
Basically, you can't show the Steam Overlay over WebView2 correctly. There doesn't seem to be any good way to do it. DirectComposition is the best approach, but the difference in handling of alpha makes it look wrong. Maybe there's a workaround involving something truly horrendous like trying to hook in after the Steam Overlay does, but I don't really want to get in to something that ugly.
It would be great if Valve fixed this! There are three ways I think it could be solved:
- Support multi-process architectures: allow the Steam overlay to appear over a surface created in a child process. SteamDB shows that there are already thousands of games on Steam made with either Electron or NW.js, and this would make it easier to support them. It would also unblock support for WebView2. I think this is the best solution.
- Render the Steam Overlay with premultiplied alpha. Then it can be used with technologies like DirectComposition to layer it over other content.
- Redesign the Steam Overlay to be less intrusive - perhaps using something like DirectComposition itself to layer over existing app content rather than take over the rendering of the app's own surface. Then in theory it should work with any content regardless of how it's rendered and not need to hook in to the app's core logic. I guess this would require a fundamental rewrite though, so is probably least practical, unless Valve happened to be planning some big redesign anyway.
Valve support have not been helpful to me so far. I've tried to provide them with all the technical details necessary to resolve this, and opened a ticket a few weeks ago; their only response so far was a brief message repeating the requirements ("DirectX 7 - 12, OpenGL, Metal, and Vulkan"), and then nothing. Given thousands of games are already published to Steam with web technologies, and the increasing sophistication of web technology with things like the latest WebGPU rendering API bringing significant advances, I think it makes sense to better support web games for Steam. The web platform already works brilliantly for games, as we've been leading the way on with Construct, and it's only getting better. With some hopefully small changes Steam could have much better support for them and unblock us from improving our Steam support for Construct!
What else could I say I learned from this? I guess make sure documentation is comprehensive, including detailing any assumptions made; make sure error codes are useful; try to stick to best practices (like using premultipled alpha for rendering); and try to make technologies adaptable (like supporting straight alpha in DirectComposition), so that when someone like me comes along and tries to combine technologies in an interesting new way, there are more options on the table. All things that experienced developers already knew, I suppose - but I guess this case shows the kind of situation some unsuspecting developer like me can end up in if not taken seriously enough. I've fruitlessly spent a couple of days on this already and can't afford to waste more time, so back I go to my usual work on Construct, but I'll hold out hope we can make this work in future with a little co-operation from Valve!