Table of Contents

CEF Direct offscreen rendering to Direct3D Surfaces

As I'm currently working on a project that make intensive use of CEF to render imagery overlays I finally reached the point where it could become very interesting to be able to render offscreen content with CEF, but still avoid copying the generated texture from GPU to CPU and then back to GPU to inject it in another 3D application.

Until very recently, I was just waiting for the official bug report to be handled by someone from the official CEF team or at least soemone more familiar with this giant project, but this wasn't moving fast enough for me, so I finally decided I should get my hands dirty and try and fix this by myself.

The patch design

The whole idea is based on the fact that CEF used the ANGLE library on Windows to convert the GLES commands into DirectX commands, and the ANGLE library supports interactions with other DirectX devices/resources out of the box.

From there, I had to spend a long time reading the sources and documentation on CEF to try to understand how it actually works and where exactly I could inject the changes I needed. The most complex aspect in this project is due to the fact that you basically have 2 separated “parts”:

Those two parts can communicate with commands sent from the client to the service via shared memory (with optional results received back), and some complex command buffer system.

So, the start point was in my software process, where I can create DirectX surfaces with shared handle, then with ANGLE, I can normally render on this shared handle from a GL ES context, but to do so, I needed to find a way to pass the shared handle value to the GPU service side, and thus, needed to create a new command in the CEF internals.

Note that one has to be very carefull when changing the CEF public API like that: those functions prototype are then used to generate a bunch of auto-generated bindings and all, so you have to respect a specify syntax (note the comments format for instance which should be exactly like that :-) )
  1. Setup a pbuffer with the shared handle (as described on the ANGLE interop page)
  1. Setup the resources required to render a screen aligned quad: we need a program that will simply copy the input texture on the render surface. A vertex buffer to draw 2 triangles, and an index buffer.
  1. Then each time our new command is executed, we carefully replace the current context with our own context, then we copy the provided texture onto our pbuffer, and then restore the default current context, just how it was:
      // Get the current read and draw surfaces:
      EGLSurface drawSurface = eglGetCurrentSurface(EGL_DRAW);
      EGLSurface readSurface = eglGetCurrentSurface(EGL_READ);
    
      // Assign our surface as current draw/read target:
      EGLSurface ourSurface = gSurfaces[handle];
    
      if(eglMakeCurrent(egl_display, ourSurface, ourSurface, nvContext) != EGL_TRUE) {
        NV_LOG("GLES2DecoderImpl: ERROR: eglMakeCurrent failed with: "<< ui::GetLastEGLErrorString());
        // Try to restore the previous surfaces:
        eglMakeCurrent(display, drawSurface, readSurface, curContext);
        return error::kNoError;
      }
      
      // init the context if necessary:
      if(program == 0) {
        NV_LOG2("GLES2DecoderImpl: Initializing context resources.")
        nvInitContext();
      }
    
      // Convert the client texture_id to our service texture_id:
      uint32_t service_tex_id=0;
      if(GetServiceTextureId(texture_id, &service_tex_id)) {
        NV_LOG2("GLES2DecoderImpl: Drawing from service texture_id: "<<service_tex_id);
        nvDraw(service_tex_id);
      }
      else {
        NV_LOG("GLES2DecoderImpl: ERROR: Cannot retrieve service texture for client id: "<<texture_id);
      }
    
      // float v1 = 1.0f * (rand()/(float)RAND_MAX);
      // float v2 = 1.0f * (rand()/(float)RAND_MAX);
      // NV_LOG2("GLES2DecoderImpl: Clearing shared handle surface with color: ("<<v1<<","<<v2<<",0.0)");
    
      // First we try to just display some fixed color (red):
      // glViewport(0, 0, 1920, 1080);
      // glClearColor(v1, v2, 0.0f, 1.0f);
      // glClear(GL_COLOR_BUFFER_BIT);
    
      // Now that we are done, we restore the previous current context/surfaces:
      eglMakeCurrent(display, drawSurface, readSurface, curContext);
    
In the regular rendering pipeline, we rather call gl_->DeleteTextures(1, &mailbox_texture); on the client size, But I believe this will simply eventually execute the equivalent of DeleteTexturesHelper(1, &texture_id); on the service side, so I thought I could save a command here.
As you can see from the code snippets added above, the code is currently still full of commented tests, aggressive checks, debug outputs, not optimized sections, etc: So this is still a very early implementation and will probably requires some refactoring before it can be used in a general way by the CEF/chromium community :-)

The build process

During my initial tests, I was modifying the CEF/chromium sources directly, and then building CEF following the official instructions. But I quickly realized this would become a nightmare to maintain except if I were to keep it all under git control which seems even more frightening when you consider the size of this project!

So I took a different path: instead I built a set of script functions and a separated collection of CEF files that I needed to modify: then I have script functions used to:

  1. checkout a given branch of CEF (branch 3163 by default):
    # Update of the CEF sources to a given branch
    nv_cef_update() {
    
      local branch="3163"
      nv_cef_init_exports
    
      local bdir=`cygpath -w "$_cef_build_dir"`
      # echo "using build dir: $bdir"
      cd "$_cef_build_dir/chromium_git"
    
      nv_cef_call_python ../automate/automate-git.py --download-dir=$bdir/chromium_git --depot-tools-dir=$bdir/depot_tools --no-distrib --no-build --branch=$branch
    
      cd - > /dev/null
    }
  1. Override all the base files from CEF with the updated version I stored separetely and then generate the project files, this script will also take care of re-generating all the auto-generated files based on the modifications we just injected in the code:
    # create the cef projects:
    nv_cef_create_project() {
      nv_cef_init_exports
    
      # Copy all the updated files:
      cp -Rf "$_cef_patch_dir/src" "$_cef_build_dir/chromium_git/chromium/"
    
      # Once we are done copying the files ensure we call the translator tool:
      cd "$_cef_build_dir/chromium_git/chromium/src/cef/tools"
      nv_cef_call_python translator.py --root-dir ..
      cd - > /dev/null
    
      # Update the command buffer functions:
      cd "$_cef_build_dir/chromium_git/chromium/src"
      nv_cef_call_python "gpu\\command_buffer\\build_gles2_cmd_buffer.py"
      cd - > /dev/null
    
      # Create the project files:
      cd "$_cef_build_dir/chromium_git/chromium/src/cef"
      nv_cef_call_python tools/gclient_hook.py
      cd - > /dev/null
    }
  1. Then another script to perform the actual build:
    # build the cef library:
    nv_cef_build() {
      nv_cef_init_exports
      
      local btype=${1:-Release}
      cd "$_cef_build_dir/chromium_git/chromium/src"
      nv_cef_call_ninja -C "out\\${btype}_GN_x64" cef
    
      echo "Done building CEF"
      cd - > /dev/null
    }
  1. And finally another script to generate the distrib folder:
    # Make CEF distrib:
    nv_cef_make_distrib() {
      #  cf. https://bitbucket.org/chromiumembedded/cef/wiki/BranchesAndBuilding.md
      local vsdir=`nv_get_visualstudio_dir`
      vsdir=`nv_to_win_path $vsdir`
      export CEF_VCVARS="$vsdir\\VC\\bin\\amd64\\vcvars64.bat"
    
      cd "$_cef_build_dir/chromium_git/chromium/src/cef/tools"
    
      nv_cef_call_python make_distrib.py --output-dir ../binary_distrib/ --ninja-build --x64-build --allow-partial
    
      echo "Done packaging CEF"
      cd - > /dev/null
    }
Note that I'm executing all the scripts above from a cygwin environment

Do be able to use those scripts, one should only need to provide 2 folder locations:

  1. The location where CEF should be built (ie. containing the official CEF sources)
  2. The location where the patched files are (which will be the folder containing the script file itself if you use my package below)

Those location can be configured at the beginning of the script file I'm providing below:

# CEF Build dir: this is the root folder where CEF is built:
_cef_build_dir="/cygdrive/d/Projects/CEFBuild"

# CEF patch dir: this is the folder containing all the updated files required to build
# our patched version of CEF (with support for the Direct rendering to Direct3D)
_cef_patch_dir="`nv_get_project_dir`/deps/patches/cef"

Debug output systems

Currently in use in this CEF patch, you will find 2 debug output logging mechanism I created specifically for my investigations (I don't know how to use the CEF logging system properly, and in fact I didn't even want to learn that part ;-) ).

This means that in my software I can then assign a CefLogHandler instance with an overload log() method to retrieve log messages originating from CEF directly into my software logging system, which make it all more consistent from my point of view.

Both of those logging system have a verbosity controlled by the environement variable “NV_CEF_LOG_LEVEL” which is expected to take 3 different values (default value if not defined is 0):

  1. NV_CEF_LOG_LEVEL=0 ⇒ No log output at all
  2. NV_CEF_LOG_LEVEL=1 ⇒ Minimal log outputs (mainly errors if any)
  3. NV_CEF_LOG_LEVEL=2 ⇒ Maximum output level (errors and infos)

Patch files

So finally, here is a link to a github repo I just created to hold those patch files: this repo contains the cef.sh script with the functions described above, and all the modified CEF files in the src/ folder but no real file history (as all my source files are stored on a different [private] repo I have)

Github repo: https://github.com/roche-emmanuel/cef_direct3d_offscreen_rendering

⇒ If you have any question or problem you can still post a comment here or at an issue on the github repo, or contact me on linkedin/facebook/google+ or by email at roche.emmanuel (gmail account)

Additional notes

During the work on this project I also took a lot of notes on what I was doing, what was working, what was not, etc: it's a bit messy, but it still contains valuable info if you need to know more. So you can access those notes on this page: CEF direct offscreen rendering to Direct3D notes

13/12/2017 - Update: Texture resource usage investigations

I noticed during the initial usage tests for this patch that there seem to be a serious issue on the GL texture release process (textures are simply not released anymore if the requests for new frames are coming too quickly). I'm currently investigating this issue.