Getting OpenGL ES Transform Feedback data back to the host using Emscripten

Using GLFW, the C++ OpenGL ES code below calculates the square roots of five numbers and outputs them on the command line. The code makes use of Transform Feedback. When I compile on Ubuntu 17.10, using the following command, I get the result I expect:

$ g++ tf.cpp -lGL -lglfw

If I use Emscripten, however, an exception is thrown, which indicates that glMapBufferRange is only supported when access is MAP_WRITE|INVALIDATE_BUFFER. I do want to read rather than write, so perhaps I shouldn't use glMapBufferRange, but what can I use instead? I have tried on both Firefox and Chromium. The command I use to compile with Emscripten is:

$ em++ -std=c++11 tf.cpp -s USE_GLFW=3 -s USE_WEBGL2=1 -s FULL_ES3=1 -o a.out.html

The code follows:

#include <iostream>

#define GLFW_INCLUDE_ES3
#include <GLFW/glfw3.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

static const GLuint WIDTH  = 800;
static const GLuint HEIGHT = 600;
static const GLchar *vertex_shader_src =
"#version 300 es\n"
"precision mediump float;\n"
"in  float inValue;\n"
"out float outValue;\n"
"void main() {\n"
"  outValue = sqrt(inValue);\n"
"}\n";

// Emscripten complains if there's no fragment shader
static const GLchar *fragment_shader_src =
"#version 300 es\n"
"precision mediump float;\n"
"out vec4 colour;\n"
"void main() {\n"
"  colour = vec4(1.0, 1.0, 0.0, 1.0);\n"
"}\n";

static const GLfloat vertices[] = {
   0.0f,  0.5f, 0.0f,
   0.5f, -0.5f, 0.0f,
  -0.5f, -0.5f, 0.0f,
};

GLint get_shader_prog(const char *vert_src, const char *frag_src = "")
{
  enum Consts { INFOLOG_LEN = 512 };
  GLchar infoLog[INFOLOG_LEN];
  GLint fragment_shader, vertex_shader, shader_program, success;
  shader_program = glCreateProgram();

  auto mk_shader = [&](GLint &shader, const GLchar **str, GLenum shader_type) {
    shader = glCreateShader(shader_type);
    glShaderSource(shader, 1, str, NULL);
    glCompileShader(shader);
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
      glGetShaderInfoLog(shader, INFOLOG_LEN, NULL, infoLog);
      std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << '\n';
    }
    glAttachShader(shader_program, shader);
  };

  mk_shader(vertex_shader,   &vert_src, GL_VERTEX_SHADER);
  mk_shader(fragment_shader, &frag_src, GL_FRAGMENT_SHADER);

  const GLchar* feedbackVaryings[] = { "outValue" };
  glTransformFeedbackVaryings(shader_program, 1, feedbackVaryings,
                              GL_INTERLEAVED_ATTRIBS);

  glLinkProgram(shader_program);
  glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
  if (!success) {
    glGetProgramInfoLog(shader_program, INFOLOG_LEN, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << '\n';
  }
  glUseProgram(shader_program);

  glDeleteShader(vertex_shader);
  glDeleteShader(fragment_shader);
  return shader_program;
}

int main(int argc, char *argv[])
{
  glfwInit();
  glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, __FILE__, NULL, NULL);
  glfwMakeContextCurrent(window);

  GLuint shader_prog = get_shader_prog(vertex_shader_src, fragment_shader_src);
  GLint inputAttrib = glGetAttribLocation(shader_prog, "inValue");

  glViewport(0, 0, WIDTH, HEIGHT);

  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  GLfloat data[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };

  GLuint vbo;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);

  glVertexAttribPointer(inputAttrib, 1, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(inputAttrib);

  GLuint tbo;
  glGenBuffers(1, &tbo);
  glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, tbo);
  glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, sizeof(data),
               nullptr, GL_STATIC_READ);
  glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tbo);
  glEnable(GL_RASTERIZER_DISCARD);

  glBeginTransformFeedback(GL_POINTS);
  glDrawArrays(GL_POINTS, 0, 5);
  glEndTransformFeedback();

  glDisable(GL_RASTERIZER_DISCARD);
  glFlush();

  GLfloat feedback[5]{1,2,3,4,5};
  void *void_buf = glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER,0,
                                    sizeof(feedback), GL_MAP_READ_BIT);
  GLfloat *buf = static_cast<GLfloat *>(void_buf);
  for (int i = 0; i < 5; i++)
    feedback[i] = buf[i];
  glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);

  for (int i = 0; i < 5; i++)
    std::cout << feedback[i] << ' ';
  std::cout << std::endl;

  glDeleteBuffers(1, &vbo);
  glDeleteBuffers(1, &tbo);
  glDeleteVertexArrays(1, &vao);
  glfwTerminate();

  return 0;
}

1 answer

  • answered 2018-02-16 03:45 gman

    WebGL2 does not support MapBufferRange because it would be a security nightmare. Instead it supports getBufferSubData. I have no idea if that is exposed to WebAssembly in emscripten. Emscripten is emulating MapBufferRange for the cases you mentioned by using bufferData. See here and here

    If getBufferSubData is not supported you can add it. See the code in library_gl.js for how readPixels is implemented and use that as inspiration for how to expose getBufferSubData to WebAssembly. Either that or add some inline JavaScript with the _EM_ASM. I haven't done it but googling for "getBufferSubData emscripten" bought up this gist