     load_level();
     state.alive = 1;
     while (state.alive) {
-uint32_t t0 = GD.rd32(REG_FRAMES);
       draw_all();
+uint32_t t0 = millis();
       move_all();
-uint32_t t1 = GD.rd32(REG_FRAMES);
-      printf("Took %d frames\n", (t1 - t0) & 0xff);
+uint32_t t1 = millis();
+      printf("Took %d millis\n", (t1 - t0));
     }
   }
   game_over();

  // Animate conveyors: 
  byte conveyor_offset = (7 & state.t) ^ (state.conveyordir ? 0 : 7);
  uint32_t level_chars = TILES_MEM + 15 * 64 * state.level;
  GD.wr_n(level_chars + 4 * 64, state.conveyor0 + conveyor_offset, 8);
  GD.wr_n(level_chars + 4 * 64 + 3 * 8, state.conveyor1 + (7 ^ conveyor_offset), 8);

  // Willy movement
  // See http://www.seasip.demon.co.uk/Jsw/manic.mac

    GD.wr(s, nexts[r]);
}

static byte any(byte code, byte a, byte b, byte c, byte d)
{
  return ((a == code) ||
          (b == code) ||
          (c == code) ||
          (d == code));
}

// is it legal for willy to be at (x,y)
static int canbe(byte x, byte y)
{
  uint32_t addr = atxy(x / 8, y / 8);
  byte a, b, c, d;
  rd2(a, b, addr);
  rd2(c, d, addr + 32);
  return !any(ELEM_WALL, a, b, c, d);
}

// is Willy standing in a solar ray?
static int inray(byte x, byte y)
{
  uint32_t addr = atxy(x / 8, y / 8);
  byte a, b, c, d;
  rd2(a, b, addr);
  rd2(c, d, addr + 32);
  return
    (a > 0x80 ) ||
    (b > 0x80) ||
    (c > 0x80) ||
    (d > 0x80);
}

static void under_willy(byte &a, byte &b, byte &c, byte &d)
{
  uint32_t addr = atxy(state.wx / 8, state.wy / 8);
  rd2(a, b, addr);
  rd2(c, d, addr + 32);
}

static byte collide_16x16(byte x, byte y)
