auto OpenGLProgram::bind(OpenGL* instance, const Markup::Node& node, const string& pathname) -> void {
  filter = glrFilter(node["filter"].text());
  wrap = glrWrap(node["wrap"].text());
  modulo = glrModulo(node["modulo"].integer());

  string w = node["width"].text(), h = node["height"].text();
  if(w.endsWith("%")) relativeWidth = toReal(w.trimRight("%", 1L)) / 100.0;
  else absoluteWidth = w.natural();
  if(h.endsWith("%")) relativeHeight = toReal(h.trimRight("%", 1L)) / 100.0;
  else absoluteHeight = h.natural();

  format = glrFormat(node["format"].text());

  program = glCreateProgram();
  glGenFramebuffers(1, &framebuffer);

  if(file::exists({pathname, node["vertex"].text()})) {
    string source = file::read({pathname, node["vertex"].text()});
    parse(instance, source);
    vertex = glrCreateShader(program, GL_VERTEX_SHADER, source);
  } else {
    vertex = glrCreateShader(program, GL_VERTEX_SHADER, OpenGLVertexShader);
  }

  if(file::exists({pathname, node["geometry"].text()})) {
    string source = file::read({pathname, node["geometry"].text()});
    parse(instance, source);
    geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, source);
  } else {
  //geometry shaders, when attached, must pass all vertex output through to the fragment shaders
  //geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, OpenGLGeometryShader);
  }

  if(file::exists({pathname, node["fragment"].text()})) {
    string source = file::read({pathname, node["fragment"].text()});
    parse(instance, source);
    fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, source);
  } else {
    fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, OpenGLFragmentShader);
  }

  for(auto& leaf : node.find("pixmap")) {
    nall::image image({pathname, leaf.text()});
    if(!image) continue;
    image.transform();

    GLuint texture;
    glGenTextures(1, &texture);

    uint n = pixmaps.size();
    pixmaps(n).texture = texture;
    pixmaps(n).width = image.width();
    pixmaps(n).height = image.height();
    pixmaps(n).format = format;
    pixmaps(n).filter = filter;
    pixmaps(n).wrap = wrap;
    if(leaf["format"]) pixmaps(n).format = glrFormat(leaf["format"].text());
    if(leaf["filter"]) pixmaps(n).filter = glrFilter(leaf["filter"].text());
    if(leaf["wrap"]) pixmaps(n).wrap = glrWrap(leaf["wrap"].text());

    uint w = glrSize(image.width()), h = glrSize(image.height());
    uint32_t* buffer = new uint32_t[w * h]();
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, pixmaps(n).format, w, h, 0, pixmaps(n).getFormat(), pixmaps(n).getType(), buffer);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image.width(), image.height(), getFormat(), getType(), image.data());
    delete[] buffer;
  }

  OpenGLSurface::allocate();
  glrLinkProgram(program);
}

//apply manifest settings to shader source #in tags
auto OpenGLProgram::parse(OpenGL* instance, string& source) -> void {
  auto lines = source.split("\n");
  for(auto& line : lines) {
    string s = line;
    if(auto position = s.find("//")) s.resize(position());  //strip comments
    s.strip();  //remove extraneous whitespace
    if(s.match("#in ?*")) {
      s.trimLeft("#in ", 1L).strip();
      if(auto setting = instance->settings.find({s})) {
        line = {"#define ", setting().name, " ", setting().value};
      } else {
        line.reset();  //undefined variable (test in source with #ifdef)
      }
    }
  }
  source = lines.merge("\n");
}

auto OpenGLProgram::release() -> void {
  OpenGLSurface::release();
  for(auto& pixmap : pixmaps) glDeleteTextures(1, &pixmap.texture);
  pixmaps.reset();

  width = 0;
  height = 0;
  format = GL_RGBA8;
  filter = GL_LINEAR;
  wrap = GL_CLAMP_TO_BORDER;
  phase = 0;
  modulo = 0;
  absoluteWidth = 0;
  absoluteHeight = 0;
  relativeWidth = 0;
  relativeHeight = 0;
}
