text: //------------------------------------------------------------------------------
  //  dyntex-emsc.c
  //  update texture per-frame with CPU generated data
  //------------------------------------------------------------------------------
  #include <stddef.h>     /* offsetof */
  #define GL_GLEXT_PROTOTYPES
  #include <GLES2/gl2.h>
  #include <GLES2/gl2ext.h>
  #define HANDMADE_MATH_IMPLEMENTATION
  #define HANDMADE_MATH_NO_SSE
  #include "HandmadeMath.h"
  #define SOKOL_IMPL
  #define SOKOL_GLES2
  #include "sokol_gfx.h"
  #include "emsc.h"
  
  typedef struct {
      hmm_mat4 mvp;
  } vs_params_t;
  
  /* width/height must be 2^N */
  #define IMAGE_WIDTH (64)
  #define IMAGE_HEIGHT (64)
  #define LIVING (0xFFFFFFFF)
  #define DEAD (0xFF000000)
  static uint32_t pixels[IMAGE_WIDTH][IMAGE_HEIGHT];
  
  static struct {
      sg_pipeline pip;
      sg_bindings bind;
      sg_pass_action pass_action;
      float rx, ry;
      int update_count;
  } state = {
      .pass_action.colors[0] = { .action = SG_ACTION_CLEAR, .value = { 0.2f, 0.3f, 0.4f, 1.0f } }
  };
  
  static void game_of_life_init();
  static void game_of_life_update();
  static void draw();
  
  int main() {
      /* setup WebGL context */
      emsc_init("#canvas", EMSC_ANTIALIAS);
  
      /* setup sokol_gfx */
      sg_setup(&(sg_desc){0});
      assert(sg_isvalid());
  
      /* a 128x128 image with streaming-update strategy */
      sg_image img = sg_make_image(&(sg_image_desc){
          .width = IMAGE_WIDTH,
          .height = IMAGE_HEIGHT,
          .pixel_format = SG_PIXELFORMAT_RGBA8,
          .usage = SG_USAGE_STREAM,
          .min_filter = SG_FILTER_LINEAR,
          .mag_filter = SG_FILTER_LINEAR,
          .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
          .wrap_v = SG_WRAP_CLAMP_TO_EDGE
      });
  
      /* a cube vertex- and index-buffer */
      float vertices[] = {
          /* pos                  color                       uvs */
          -1.0f, -1.0f, -1.0f,    1.0f, 0.0f, 0.0f, 1.0f,     0.0f, 0.0f,
           1.0f, -1.0f, -1.0f,    1.0f, 0.0f, 0.0f, 1.0f,     1.0f, 0.0f,
           1.0f,  1.0f, -1.0f,    1.0f, 0.0f, 0.0f, 1.0f,     1.0f, 1.0f,
          -1.0f,  1.0f, -1.0f,    1.0f, 0.0f, 0.0f, 1.0f,     0.0f, 1.0f,
  
          -1.0f, -1.0f,  1.0f,    0.0f, 1.0f, 0.0f, 1.0f,     0.0f, 0.0f,
           1.0f, -1.0f,  1.0f,    0.0f, 1.0f, 0.0f, 1.0f,     1.0f, 0.0f,
           1.0f,  1.0f,  1.0f,    0.0f, 1.0f, 0.0f, 1.0f,     1.0f, 1.0f,
          -1.0f,  1.0f,  1.0f,    0.0f, 1.0f, 0.0f, 1.0f,     0.0f, 1.0f,
  
          -1.0f, -1.0f, -1.0f,    0.0f, 0.0f, 1.0f, 1.0f,     0.0f, 0.0f,
          -1.0f,  1.0f, -1.0f,    0.0f, 0.0f, 1.0f, 1.0f,     1.0f, 0.0f,
          -1.0f,  1.0f,  1.0f,    0.0f, 0.0f, 1.0f, 1.0f,     1.0f, 1.0f,
          -1.0f, -1.0f,  1.0f,    0.0f, 0.0f, 1.0f, 1.0f,     0.0f, 1.0f,
  
           1.0f, -1.0f, -1.0f,    1.0f, 0.5f, 0.0f, 1.0f,     0.0f, 0.0f,
           1.0f,  1.0f, -1.0f,    1.0f, 0.5f, 0.0f, 1.0f,     1.0f, 0.0f,
           1.0f,  1.0f,  1.0f,    1.0f, 0.5f, 0.0f, 1.0f,     1.0f, 1.0f,
           1.0f, -1.0f,  1.0f,    1.0f, 0.5f, 0.0f, 1.0f,     0.0f, 1.0f,
  
          -1.0f, -1.0f, -1.0f,    0.0f, 0.5f, 1.0f, 1.0f,     0.0f, 0.0f,
          -1.0f, -1.0f,  1.0f,    0.0f, 0.5f, 1.0f, 1.0f,     1.0f, 0.0f,
           1.0f, -1.0f,  1.0f,    0.0f, 0.5f, 1.0f, 1.0f,     1.0f, 1.0f,
           1.0f, -1.0f, -1.0f,    0.0f, 0.5f, 1.0f, 1.0f,     0.0f, 1.0f,
  
          -1.0f,  1.0f, -1.0f,    1.0f, 0.0f, 0.5f, 1.0f,     0.0f, 0.0f,
          -1.0f,  1.0f,  1.0f,    1.0f, 0.0f, 0.5f, 1.0f,     1.0f, 0.0f,
           1.0f,  1.0f,  1.0f,    1.0f, 0.0f, 0.5f, 1.0f,     1.0f, 1.0f,
           1.0f,  1.0f, -1.0f,    1.0f, 0.0f, 0.5f, 1.0f,     0.0f, 1.0f
      };
      uint16_t indices[] = {
          0, 1, 2,  0, 2, 3,
          6, 5, 4,  7, 6, 4,
          8, 9, 10,  8, 10, 11,
          14, 13, 12,  15, 14, 12,
          16, 17, 18,  16, 18, 19,
          22, 21, 20,  23, 22, 20
      };
      sg_buffer vbuf = sg_make_buffer(&(sg_buffer_desc){
          .data = SG_RANGE(vertices)
      });
      sg_buffer ibuf = sg_make_buffer(&(sg_buffer_desc){
          .type = SG_BUFFERTYPE_INDEXBUFFER,
          .data = SG_RANGE(indices)
      });
  
      /* define the resource bindings */
      state.bind = (sg_bindings){
          .vertex_buffers[0] = vbuf,
          .index_buffer = ibuf,
          .fs_images[0] = img
      };
  
      /* a shader to render textured cube */
      sg_shader shd = sg_make_shader(&(sg_shader_desc){
          .attrs = {
              [0].name = "position",
              [1].name = "color0",
              [2].name = "texcoord0"
          },
          .vs.uniform_blocks[0] = {
              .size = sizeof(vs_params_t),
              .uniforms = {
                  [0] = { .name="mvp", .type=SG_UNIFORMTYPE_MAT4 }
              }
          },
          .fs.images[0] = { .name="tex", .image_type=SG_IMAGETYPE_2D },
          .vs.source =
              "uniform mat4 mvp;\n"
              "attribute vec4 position;\n"
              "attribute vec4 color0;\n"
              "attribute vec2 texcoord0;\n"
              "varying vec2 uv;"
              "varying vec4 color;"
              "void main() {\n"
              "  gl_Position = mvp * position;\n"
              "  uv = texcoord0;\n"
              "  color = color0;\n"
              "}\n",
          .fs.source =
              "precision mediump float;\n"
              "uniform sampler2D tex;\n"
              "varying vec4 color;\n"
              "varying vec2 uv;\n"
              "void main() {\n"
              "  gl_FragColor = texture2D(tex, uv) * color;\n"
              "}\n"
      });
  
      /* a pipeline-state-object for the textured cube */
      state.pip = sg_make_pipeline(&(sg_pipeline_desc){
          .layout = {
              .attrs = {
                  [0].format=SG_VERTEXFORMAT_FLOAT3,
                  [1].format=SG_VERTEXFORMAT_FLOAT4,
                  [2].format=SG_VERTEXFORMAT_FLOAT2
              }
          },
          .shader = shd,
          .index_type = SG_INDEXTYPE_UINT16,
          .depth = {
              .compare = SG_COMPAREFUNC_LESS_EQUAL,
              .write_enabled = true
          },
          .cull_mode = SG_CULLMODE_BACK
      });
  
      /* initial game-of-life seed state */
      game_of_life_init();
  
      /* hand off control to browser loop */
      emscripten_set_main_loop(draw, 0, 1);
      return 0;
  }
  
  void draw() {
      /* model-view-projection matrix from rotated model matrix */
      hmm_mat4 proj = HMM_Perspective(60.0f, (float)emsc_width()/(float)emsc_height(), 0.01f, 10.0f);
      hmm_mat4 view = HMM_LookAt(HMM_Vec3(0.0f, 1.5f, 4.0f), HMM_Vec3(0.0f, 0.0f, 0.0f), HMM_Vec3(0.0f, 1.0f, 0.0f));
      hmm_mat4 view_proj = HMM_MultiplyMat4(proj, view);
      state.rx += 0.1f; state.ry += 0.2f;
      hmm_mat4 rxm = HMM_Rotate(state.rx, HMM_Vec3(1.0f, 0.0f, 0.0f));
      hmm_mat4 rym = HMM_Rotate(state.ry, HMM_Vec3(0.0f, 1.0f, 0.0f));
      hmm_mat4 model = HMM_MultiplyMat4(rxm, rym);
      const vs_params_t vs_params = {
          .mvp = HMM_MultiplyMat4(view_proj, model)
      };
  
      /* update game-of-life state */
      game_of_life_update();
  
      /* update the dynamic image */
      sg_update_image(state.bind.fs_images[0], &(sg_image_data){ .subimage[0][0] = SG_RANGE(pixels) });
  
      /* draw pass */
      sg_begin_default_pass(&state.pass_action, emsc_width(), emsc_height());
      sg_apply_pipeline(state.pip);
      sg_apply_bindings(&state.bind);
      sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, &SG_RANGE(vs_params));
      sg_draw(0, 36, 1);
      sg_end_pass();
      sg_commit();
  }
  
  void game_of_life_init() {
      for (int y = 0; y < IMAGE_HEIGHT; y++) {
          for (int x = 0; x < IMAGE_WIDTH; x++) {
              if ((rand() & 255) > 230) {
                  pixels[y][x] = LIVING;
              }
              else {
                  pixels[y][x] = DEAD;
              }
          }
      }
  }
  
  void game_of_life_update() {
      for (int y = 0; y < IMAGE_HEIGHT; y++) {
          for (int x = 0; x < IMAGE_WIDTH; x++) {
              int num_living_neighbours = 0;
              for (int ny = -1; ny < 2; ny++) {
                  for (int nx = -1; nx < 2; nx++) {
                      if ((nx == 0) && (ny == 0)) {
                          continue;
                      }
                      if (pixels[(y+ny)&(IMAGE_HEIGHT-1)][(x+nx)&(IMAGE_WIDTH-1)] == LIVING) {
                          num_living_neighbours++;
                      }
                  }
              }
              /* any live cell... */
              if (pixels[y][x] == LIVING) {
                  if (num_living_neighbours < 2) {
                      /* ... with fewer than 2 living neighbours dies, as if caused by underpopulation */
                      pixels[y][x] = DEAD;
                  }
                  else if (num_living_neighbours > 3) {
                      /* ... with more than 3 living neighbours dies, as if caused by overpopulation */
                      pixels[y][x] = DEAD;
                  }
              }
              else if (num_living_neighbours == 3) {
                  /* any dead cell with exactly 3 living neighbours becomes a live cell, as if by reproduction */
                  pixels[y][x] = LIVING;
              }
          }
      }
      if (state.update_count++ > 240) {
          game_of_life_init();
          state.update_count = 0;
      }
  }
  