Trying to show the Steam Overlay over WebView2

21
Official Construct Team Post
Ashley's avatar
Ashley
  • 14 Aug, 2023
  • 2,208 words
  • ~9-15 mins
  • 4,111 visits
  • 2 favourites

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.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.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!

Subscribe

Get emailed when there are new posts!

  • 10 Comments

  • Order by
Want to leave a comment? Login or Register an account!
  • It's a shame because since Tauri also uses WebView2, it also suffers from the same issues.

    github.com/tauri-apps/tauri/issues/6196

    github.com/MicrosoftEdge/WebView2Feedback/issues/3200

    The good news is that your blog can probably give a good amount of context for other developers out there trying to make that exact thing work.

  • Those must have been quite desperate days, thanks for the effort... It's a pity that they have such an ignorant approach.

  • Steam's documentation mentions this problem, and indicates that some users use CEF to solve this problem.

  • If anyone can find a solution, it's you. at some point you will get it

  • I have a game that supports coop on the same screen and I have a very annoying problem, because when I use Steam Remote Play, the sound is simply not transmitted along with the image, my hope was that with Webview 2, this problem would be solved, then we have an even more serious problem, because the overlay even opens, but in no way is it closed, making it impossible to use Steam Remote Play.

    The way out is to continue using nwjs.

  • Along similar lines, I have never seen the Steam Overlay work with MacOS, whether greenworks, electron, nw.js and/or steamworks-js. It probably needs a similar architecture as what you are describing here, but Mac specific.

  • Thanks for the effort, i don‘t think this was wasted time, you‘re threading new ground after all so hopefully it will lead to a good solution down the line but i'm an optimist :) One step after the other.

    Perhaps, when you got a bit of time and are curious as well, it would be interesting to see if the Epic Games Store overlays work with Webview? Might be a good leverage against Valve :P

  • A shame but I appreciate the efforts to ensure proper integration into steam and other platforms. This is the not the type of thing most of us using C3 want to deal with!

  • Al menos demuestra que te estás esforzando para Construct y dudo que otros proyectos y sus creadores lo hagan, eso como usuario es muy valioso. Gracias!