/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2006,2007  Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/machine/memory.h>
#include <grub/time.h>
#include <grub/video.h>
#include <grub/types.h>
#include <grub/dl.h>
#include <grub/misc.h>
#include <grub/normal.h>
#include <grub/arg.h>
#include <grub/mm.h>
#include <grub/font.h>
#include <grub/term.h>
#include <grub/bitmap.h>
#include <grub/bitmap_scale.h>
#include <grub/machine/vbe.h>   /* for grub_vbe_bios_set_display_start () test */
/* Option array indices. */
#define ARGINDEX_TEST_TIME 0
#define ARGINDEX_DOUBLE_BUF 1

static const struct grub_arg_option arg_options[] = {
  {"time", 't', 0, "Time to run each test, in seconds.", 0, ARG_TYPE_INT},
  {"dbuf", 'd', 0, "Use double buffered graphics.", 0, ARG_TYPE_NONE},
  {0, 0, 0, 0, 0, 0}
};

#define DEFAULT_TEST_TIME 5

/* Command options -- populated base on command line arguments. */
struct videotest_options
{
  int test_time;
  int double_buffering;
};


static void
basic_video_test (struct videotest_options *vt_opts)
{
  int mode_type = GRUB_VIDEO_MODE_TYPE_INDEX_COLOR;
  if (vt_opts->double_buffering)
    mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED;
  if (grub_video_setup (1024, 768,
                        GRUB_VIDEO_MODE_TYPE_INDEX_COLOR) != GRUB_ERR_NONE)
    return;

  grub_getkey ();

  grub_video_color_t color;
  unsigned int x;
  unsigned int y;
  unsigned int width;
  unsigned int height;
  int i;
  grub_font_t font;
  struct grub_font_glyph *glyph;
  struct grub_video_render_target *text_layer;
  grub_video_color_t palette[16];

  grub_video_get_viewport (&x, &y, &width, &height);

  grub_video_create_render_target (&text_layer, width, height,
                                   GRUB_VIDEO_MODE_TYPE_RGB
                                   | GRUB_VIDEO_MODE_TYPE_ALPHA);

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);

  color = grub_video_map_rgb (0, 0, 0);
  grub_video_fill_rect (color, 0, 0, width, height);

  color = grub_video_map_rgb (255, 0, 0);
  grub_video_fill_rect (color, 0, 0, 100, 100);

  color = grub_video_map_rgb (0, 255, 255);
  grub_video_fill_rect (color, 100, 100, 100, 100);

  font = grub_font_get ("Helvetica Bold 14");
  if (! font)
    {
      grub_error (GRUB_ERR_BAD_FONT, "No font loaded.");
      return;
    }

  glyph = grub_font_get_glyph (font, '*');  
  grub_video_blit_glyph (glyph, color, 200 ,0);

  grub_video_set_viewport (x + 150, y + 150,
                           width - 150 * 2, height - 150 * 2);
  color = grub_video_map_rgb (77, 33, 77);
  grub_video_fill_rect (color, 0, 0, width, height);

  grub_video_set_active_render_target (text_layer);

  color = grub_video_map_rgb (255, 255, 255);

  glyph = grub_font_get_glyph (font, 'A');
  grub_video_blit_glyph (glyph, color, 16, 16);
  glyph = grub_font_get_glyph (font, 'B');
  grub_video_blit_glyph (glyph, color, 16 * 2, 16);

  glyph = grub_font_get_glyph (font, '*');

  for (i = 0; i < 16; i++)
    {
      color = grub_video_map_color (i);
      palette[i] = color;
      grub_video_blit_glyph (glyph, color, 16 + i * 16, 32);
    }

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);

  for (i = 0; i < 255; i++)
    {
      color = grub_video_map_rgb (i, 33, 77);
      grub_video_fill_rect (color, 0, 0, width, height);
      grub_video_blit_render_target (text_layer, GRUB_VIDEO_BLIT_BLEND, 0, 0,
                                     0, 0, width, height);
    }

  grub_video_swap_buffers ();
  grub_getkey ();

  /* Test VBE set display start address. */
  /* This should scroll the screen, first vertically,
   * then horizontally.  The horizontal scrolling seems to
   * only have a resolution of about 16 pixels on my VIA Mini-ITX. */
  int vbestatus;
  int vbeok = 0;
  int vbeerr = 0;
  for (i = 0; i < 50; i++)
    {
      vbestatus = grub_vbe_bios_set_display_start (0, i);
      if (vbestatus == GRUB_VBE_STATUS_OK)
        vbeok++;
      else
        vbeerr++;
    }


  grub_getkey ();

  for (i = 0; i < 50; i++)
    {
      vbestatus = grub_vbe_bios_set_display_start (i, 50);
      if (vbestatus == GRUB_VBE_STATUS_OK)
        vbeok++;
      else
        vbeerr++;
    }

  grub_getkey ();

  grub_video_delete_render_target (text_layer);
  grub_video_restore ();

  grub_printf ("VBE set_display_start status: %d\n", vbestatus);
  grub_printf ("ok: %d\n", vbeok);
  grub_printf ("errors: %d\n", vbeerr);

  grub_errno = GRUB_ERR_NONE;
}



/** 
 * Simple opaque image blit test. 
 * Returns the error status in grub_errno.
 */
static void
bitmap_demo (struct videotest_options *vt_opts)
{
  int mode_type = GRUB_VIDEO_MODE_TYPE_RGB;
  if (vt_opts->double_buffering)
    mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED;
  if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE)
    return;

  grub_video_rect_t view;
  grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y,
                           (unsigned *) &view.width,
                           (unsigned *) &view.height);

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);
  const int bm1width = 500, bm1height = 400;
  struct grub_video_bitmap *bitmap1;

  if (grub_video_bitmap_create (&bitmap1, bm1width, bm1height,
                                GRUB_VIDEO_BLIT_FORMAT_RGB_888)
      != GRUB_ERR_NONE)
    return;

  int offset = 0;
  int x;
  int y;
  grub_uint8_t *data = grub_video_bitmap_get_data (bitmap1);
  for (y = 0; y < bm1height; y++)
    {
      for (x = 0; x < bm1width; x++)
        {
          data[offset++] = x ^ y;       /* red */
          data[offset++] = (x * 3) ^ (y * 3);   /* green */
          data[offset++] = (x * 2) ^ (y * 2);   /* blue */
        }
    }

  /* Blit the entire bitmap in the center of the screen. */
  grub_video_blit_bitmap (bitmap1,
                          GRUB_VIDEO_BLIT_REPLACE,
                          view.x + (view.width - bm1width) / 2,
                          view.y + (view.height - bm1height) / 2,
                          0, 0, bm1width, bm1height);
  grub_video_swap_buffers ();
  grub_getkey ();

  /* Blit more copies of the bitmap. */
  /* Upper left. */
  grub_video_blit_bitmap (bitmap1, GRUB_VIDEO_BLIT_REPLACE,
                          view.x, view.y, 0, 0, bm1width, bm1height);
  /* Upper right. */
  grub_video_blit_bitmap (bitmap1,
                          GRUB_VIDEO_BLIT_REPLACE,
                          view.x + view.width - bm1width,
                          view.y, 0, 0, bm1width, bm1height);
  /* Lower left. */
  grub_video_blit_bitmap (bitmap1,
                          GRUB_VIDEO_BLIT_REPLACE,
                          view.x,
                          view.y + view.height - bm1height,
                          0, 0, bm1width, bm1height);
  /* Lower right. */
  grub_video_blit_bitmap (bitmap1,
                          GRUB_VIDEO_BLIT_REPLACE,
                          view.x + view.width - bm1width,
                          view.y + view.height - bm1height,
                          0, 0, bm1width, bm1height);
  grub_video_swap_buffers ();
  grub_getkey ();

  int clearbg = 0;              /* Boolean flag: whether to fill background. */
  grub_video_color_t bgcolor = grub_video_map_rgb (16, 16, 96);

  /* Animate the image sliding in. End when a key is pressed. */
  int vscale = 1000;
  int velocityx = -5000;
  int velocityy = -8000;
  int positionx = 100;
  int positiony = 300;

  grub_uint32_t frame_count = 0;
  grub_uint32_t start_time = grub_get_time_ms ();

  while (grub_checkkey () == -1)
    {
      /* If the time limit option is set, then check if it's exceeded. */
      if (vt_opts->test_time != 0
          && grub_get_time_ms () >= start_time + vt_opts->test_time * 1000)
        break;

      int newx = positionx + velocityx / vscale;
      int newy = positiony + velocityy / vscale;

      /* Check collision w/ left */
      if (newx < view.x && velocityx < 0)
        {
          velocityx = -velocityx;
          newx = positionx + velocityx / vscale;
        }
      /* Check collision w/ right */
      if (newx + bm1width > view.x + view.width && velocityx > 0)
        {
          velocityx = -velocityx;
          newx = positionx + velocityx / vscale;
        }
      /* Check collision w/ top */
      if (newy < 0 && velocityy < 0)
        {
          velocityy = -velocityy;
          newy = positiony + velocityy / vscale;
        }
      /* Check collision w/ bottom */
      if (newy + bm1height > view.y + view.height && velocityy > 0)
        {
          velocityy = -velocityy;
          newy = positiony + velocityy / vscale;
        }

      positionx = newx;
      positiony = newy;

      if (clearbg)
        grub_video_fill_rect (bgcolor, view.x, view.y, view.width,
                              view.height);

      grub_video_blit_bitmap (bitmap1,
                              GRUB_VIDEO_BLIT_REPLACE,
                              view.x + positionx,
                              view.y + positiony, 0, 0, bm1width, bm1height);
      grub_video_swap_buffers ();
      frame_count++;

      /* Acceleration due to  gravity... 
       * note that the y coordinate is increasing downward. */
      velocityy += vscale / 7;
    }

  /* Calculate average frame rate. */
  grub_uint32_t duration = grub_get_time_ms () - start_time;
  grub_uint32_t fps_x10 = 10 * 1000 * frame_count / duration;

  /* Eat the keystroke. */
  if (grub_checkkey () != -1)
    grub_getkey ();

  grub_video_bitmap_destroy (bitmap1);
  grub_video_restore ();

  grub_printf ("Average frame rate: %d.%d fps\n", fps_x10 / 10, fps_x10 % 10);

  grub_errno = GRUB_ERR_NONE;
}



/* Configuration settings for a benchmark run. */
struct benchmark_config
{
  int width;
  int height;
  unsigned int mode_type;
  int use_rgba_bitmaps;
};

/* Macro to make the benchmark_configs[] declaration more concise. */
#define MODE_TYPE_RGB(bpp) (GRUB_VIDEO_MODE_TYPE_RGB \
                            | ((bpp) << GRUB_VIDEO_MODE_TYPE_DEPTH_POS))

/* The video configurations to use for the benchmark. */
static struct benchmark_config benchmark_configs[] = {
  {320, 200, MODE_TYPE_RGB (0), 0},
  {640, 480, MODE_TYPE_RGB (0), 0},
  {1024, 768, MODE_TYPE_RGB (0), 0},
  {1024, 768, MODE_TYPE_RGB (0), 1},
};

#define NUM_BENCHMARK_CONFIGS (sizeof(benchmark_configs) \
                               / sizeof(benchmark_configs[0]))

struct benchmark_result
{
  /* If set to 1, the test was able to run successfully; 
   * 0 means there was an error. */
  int test_passed;

  /* Bits per pixel for the video mode used. */
  int bpp;

  /* All fps are in fps * 10 to achieve one decimal place. */
  /* Set to 0 to indicate the test could not be run. */
  grub_int32_t fill_fps;
  grub_int32_t blit_fps;
  grub_int32_t blend_fps;
};

#define BENCHMARK_RESULT_FPS_SCALE 10

static void
move_rectangle_one_step (int *x, int *y,
                         int width, int height,
                         int *vx, int *vy, const grub_video_rect_t * bounds)
{
  int newx = *x + *vx;
  int newy = *y + *vy;

  /* Check collision w/ left */
  if (newx < bounds->x && *vx < 0)
    {
      *vx = -*vx;
      newx = *x + *vx;
    }
  /* Check collision w/ right */
  if (newx + width > bounds->x + bounds->width && *vx > 0)
    {
      *vx = -*vx;
      newx = *x + *vx;
    }
  /* Check collision w/ top */
  if (newy < 0 && *vy < 0)
    {
      *vy = -*vy;
      newy = *y + *vy;
    }
  /* Check collision w/ bottom */
  if (newy + height > bounds->y + bounds->height && *vy > 0)
    {
      *vy = -*vy;
      newy = *y + *vy;
    }

  *x = newx;
  *y = newy;
}

/**
 * Run the benchmark test for a particular video mode, which is specified 
 * by ``*config``.  The results of the test are stored in ``*result``.
 */
static void
do_benchmark (const struct benchmark_config *config,
              struct benchmark_result *result,
              struct videotest_options *vt_opts)
{
  struct grub_video_mode_info modeinfo;

  result->test_passed = 0;
  result->fill_fps = 0;
  result->blit_fps = 0;
  result->blend_fps = 0;
  result->bpp = 0;

  int mode_type = config->mode_type;
  if (vt_opts->double_buffering)
    mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED;
  if (grub_video_setup (config->width, config->height,
                        mode_type) != GRUB_ERR_NONE)
    return;

  if (grub_video_get_info (&modeinfo) == GRUB_ERR_NONE)
    result->bpp = modeinfo.bpp;

  /* Full screen bitmap blit test. */

  grub_video_rect_t view;
  grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y,
                           (unsigned *) &view.width,
                           (unsigned *) &view.height);

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);


  /* For measuring timing. */
  grub_uint32_t frame_count;
  grub_uint64_t start_time;
  grub_uint64_t desired_stop_time;
  grub_uint32_t duration;


  /*** FILL TEST ***/

  /* Alternates between 0 and 1 to change the color. */
  int color_flag = 0;
  grub_video_color_t fillcolors[2];
  fillcolors[0] = grub_video_map_rgb (0, 0, 255);
  fillcolors[1] = grub_video_map_rgb (255, 255, 0);

  frame_count = 0;
  start_time = grub_get_time_ms ();
  desired_stop_time = start_time + vt_opts->test_time * 1000;

  while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1)
    {
      grub_video_fill_rect (fillcolors[color_flag],
                            view.x, view.y, view.width, view.height);
      grub_video_swap_buffers ();
      color_flag ^= 1;
      frame_count++;
    }
  if (grub_checkkey () != -1)
    grub_getkey ();             /* Eat the keypress, if there was one. */

  /* Calculate average frame rate. */
  duration = grub_get_time_ms () - start_time;
  if (duration == 0)
    {
      result->fill_fps = 0;
    }
  else
    {
      result->fill_fps =
        (BENCHMARK_RESULT_FPS_SCALE * 1000
         * frame_count / duration);
    }


  /*** BLIT TEST ***/

  /* Generate two bitmaps, the same size as the screen. */
  const int bitmapwidth = view.width, bitmapheight = view.height;
  struct grub_video_bitmap *bitmap1;
  struct grub_video_bitmap *bitmap2;
  enum grub_video_blit_format bitmap_format = config->use_rgba_bitmaps
    ? GRUB_VIDEO_BLIT_FORMAT_RGBA_8888 : GRUB_VIDEO_BLIT_FORMAT_RGB_888;

  if (grub_video_bitmap_create (&bitmap1, bitmapwidth, bitmapheight,
                                bitmap_format) != GRUB_ERR_NONE)
    return;

  if (grub_video_bitmap_create (&bitmap2, bitmapwidth, bitmapheight,
                                bitmap_format) != GRUB_ERR_NONE)
    {
      grub_video_bitmap_destroy (bitmap1);
      return;
    }

  int offset;
  int x;
  int y;
  grub_uint8_t *data;

  offset = 0;
  data = grub_video_bitmap_get_data (bitmap1);
  for (y = 0; y < bitmapheight; y++)
    {
      for (x = 0; x < bitmapwidth; x++)
        {
          data[offset++] = x ^ y;       /* red */
          data[offset++] = (x * 3) ^ (y * 3);   /* green */
          data[offset++] = (x * 2) ^ (y * 2);   /* blue */
          if (config->use_rgba_bitmaps)
            data[offset++] = 255;
        }
    }

  offset = 0;
  data = grub_video_bitmap_get_data (bitmap2);
  for (y = 0; y < bitmapheight; y++)
    {
      for (x = 0; x < bitmapwidth; x++)
        {
          data[offset++] = x + y;       /* red */
          data[offset++] = x * x + y * y;       /* green */
          data[offset++] = x * x / 4 + y * y / 4;       /* blue */
          if (config->use_rgba_bitmaps)
            data[offset++] = 255;
        }
    }


  /* Now do the blit test, alternating between the two bitmaps. */
  frame_count = 0;
  start_time = grub_get_time_ms ();
  desired_stop_time = start_time + vt_opts->test_time * 1000;

  int cur = 0;                  /* Which bitmap to draw this frame. */
  while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1)
    {
      struct grub_video_bitmap *current_bitmap = cur == 0 ? bitmap1 : bitmap2;
      grub_video_blit_bitmap (current_bitmap,
                              GRUB_VIDEO_BLIT_REPLACE,
                              view.x, view.y, 0, 0, bitmapwidth,
                              bitmapheight);
      grub_video_swap_buffers ();
      frame_count++;
      cur ^= 1;
    }
  if (grub_checkkey () != -1)
    grub_getkey ();             /* Eat the keypress, if there was once. */

  /* Calculate average frame rate. */
  duration = grub_get_time_ms () - start_time;
  if (duration == 0)
    {
      result->blit_fps = 0;
    }
  else
    {
      result->blit_fps =
        (BENCHMARK_RESULT_FPS_SCALE * 1000
         * frame_count / duration);
    }

  grub_video_bitmap_destroy (bitmap2);
  grub_video_bitmap_destroy (bitmap1);



  /*** BLEND TEST ***/

  /* Generate two bitmaps, with alpha translucency. */
  const int bbw = view.width * 2 / 3;
  const int bbh = view.height * 2 / 3;

  if (grub_video_bitmap_create (&bitmap1, bbw, bbh,
                                GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) !=
      GRUB_ERR_NONE)
    return;

  if (grub_video_bitmap_create (&bitmap2, bbw, bbh,
                                GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) !=
      GRUB_ERR_NONE)
    {
      grub_video_bitmap_destroy (bitmap1);
      return;
    }

  offset = 0;
  data = grub_video_bitmap_get_data (bitmap1);
  for (y = 0; y < bbh; y++)
    {
      for (x = 0; x < bbw; x++)
        {
          /* Calculate a to be increasing away from the center. */
          int dx = 256 * (x - bbw / 2) / (bbw / 2);
          int dy = 256 * (y - bbh / 2) / (bbh / 2);
          int a = dx * dx + dy * dy;
          /* range for a = 0 .. 2*(256^2) = 2*2^16 = 2^17 */
          a >>= 17 - 8;         /* Make range 0..256. */
          if (a > 255)
            a = 255;

          data[offset++] = x ^ y;       /* red */
          data[offset++] = (x * 3) ^ (y * 3);   /* green */
          data[offset++] = (x * 2) ^ (y * 2);   /* blue */
          data[offset++] = 255 - a;
        }
    }

  offset = 0;
  data = grub_video_bitmap_get_data (bitmap2);
  for (y = 0; y < bbh; y++)
    {
      for (x = 0; x < bbw; x++)
        {
          data[offset++] = x + y;       /* red */
          data[offset++] = x * x + y * y;       /* green */
          data[offset++] = x * x / 4 + y * y / 4;       /* blue */
          data[offset++] = 255;
        }
    }

  frame_count = 0;
  start_time = grub_get_time_ms ();
  desired_stop_time = start_time + vt_opts->test_time * 1000;


  grub_video_color_t bgcolor = grub_video_map_rgb (80, 80, 80);

  /* Bitmap locations. */
  int b1x = 0;
  int b1y = 0;
  int b2x = view.width - bbw;
  int b2y = 0;
  /* Bitmap velocities. */
  int b1vx = 8;
  int b1vy = 12;
  int b2vx = -10;
  int b2vy = 9;

  while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1)
    {
      move_rectangle_one_step (&b1x, &b1y, bbw, bbh, &b1vx, &b1vy, &view);
      move_rectangle_one_step (&b2x, &b2y, bbw, bbh, &b2vx, &b2vy, &view);
      grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height);
      grub_video_blit_bitmap (bitmap2,
                              GRUB_VIDEO_BLIT_BLEND,
                              b2x, b2y, 0, 0, bbw, bbh);
      grub_video_blit_bitmap (bitmap1,
                              GRUB_VIDEO_BLIT_BLEND,
                              b1x, b1y, 0, 0, bbw, bbh);
      grub_video_swap_buffers ();
      frame_count++;
      cur ^= 1;
    }
  if (grub_checkkey () != -1)
    grub_getkey ();             /* Eat the keypress, if there was once. */

  /* Calculate average frame rate. */
  duration = grub_get_time_ms () - start_time;
  if (duration == 0)
    {
      result->blend_fps = 0;
    }
  else
    {
      result->blend_fps =
        (BENCHMARK_RESULT_FPS_SCALE * 1000
         * frame_count / duration);
    }

  grub_video_bitmap_destroy (bitmap2);
  grub_video_bitmap_destroy (bitmap1);

  grub_video_restore ();
  result->test_passed = 1;
}

/**
 * Run a benchmark test in a series of video modes.
 * The results are reported in tabular form.  This will be helpful to
 * determine how effective various optimizations are.
 */
static void
benchmark_test (struct videotest_options *vt_opts)
{
  unsigned int i;
  struct benchmark_result results[NUM_BENCHMARK_CONFIGS];

  /* Set option default values. */
  if (vt_opts->test_time == 0)
    vt_opts->test_time = DEFAULT_TEST_TIME;

  /* Run benchmarks. */
  for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++)
    {
      grub_error_push ();
      do_benchmark (&benchmark_configs[i], &results[i], vt_opts);
    }

  grub_print_error ();

  /* Display results. */
  grub_printf ("Benchmark results (in frames/s):\n");
  grub_printf ("(W=Width, H=Height, B=Bits per pixel,\n"
               " T=Mode Type, A=Bitmap Alpha)\n");
  grub_printf ("   W    H  B T A    FILL    BLIT   BLEND\n");
  for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++)
    {
      struct benchmark_config *c = &benchmark_configs[i];
      struct benchmark_result *r = &results[i];

      if (r->test_passed)
        {
          grub_printf ("%4dx%4d %2d %d %d: %4d.%d  %4d.%d  %4d.%d\n",
                       c->width, c->height, r->bpp,
                       c->mode_type & 0x0F,
                       c->use_rgba_bitmaps,
                       r->fill_fps / BENCHMARK_RESULT_FPS_SCALE,
                       r->fill_fps % BENCHMARK_RESULT_FPS_SCALE,
                       r->blit_fps / BENCHMARK_RESULT_FPS_SCALE,
                       r->blit_fps % BENCHMARK_RESULT_FPS_SCALE,
                       r->blend_fps / BENCHMARK_RESULT_FPS_SCALE,
                       r->blend_fps % BENCHMARK_RESULT_FPS_SCALE);
        }
      else
        {
          grub_printf ("%4dx%4d %2d %d  Not supported.\n",
                       c->width, c->height,
                       ((c->mode_type & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK)
                        >> GRUB_VIDEO_MODE_TYPE_DEPTH_POS),
                       c->mode_type & 0x0F);
        }
    }

  grub_errno = GRUB_ERR_NONE;
}


/**
 * Test time functions.
 */
static void
clock_test (struct videotest_options *vt_opts)
{
  int mode_type = GRUB_VIDEO_MODE_TYPE_RGB;
  if (vt_opts->double_buffering)
    mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED;
  if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE)
    return;

  grub_video_rect_t view;
  grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y,
                           (unsigned *) &view.width,
                           (unsigned *) &view.height);

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);

  /* Draw a progress bar that animates in sync with time. */

  grub_video_rect_t bar_frame;
  bar_frame.width = view.width - 2 * view.width / 10;
  bar_frame.height = view.height / 20;
  bar_frame.x = view.x + view.width / 10;
  bar_frame.y = view.y + view.height - bar_frame.height - view.height / 10;

  grub_video_color_t bgcolor = grub_video_map_rgb (50, 50, 50);
  grub_video_color_t framecolor = grub_video_map_rgb (255, 255, 255);
  grub_video_color_t barbgcolor = grub_video_map_rgb (0, 0, 128);
  grub_video_color_t barcolor = grub_video_map_rgb (100, 100, 255);

  grub_uint32_t frame_count;
  grub_uint64_t start_time;
  grub_uint64_t barstart;
  grub_uint32_t barlength = 1000;

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);

  frame_count = 0;
  start_time = grub_get_time_ms ();
  barstart = grub_get_time_ms ();

  while (grub_checkkey () == -1)
    {
      grub_uint64_t now;
      grub_uint32_t bartime;
      now = grub_get_time_ms ();
      /* If the time limit option is set, then check if it's exceeded. */
      if (vt_opts->test_time != 0
          && now >= start_time + vt_opts->test_time * 1000)
        break;
      bartime = now - barstart;
      if (bartime > barlength)
        {
          barstart = grub_get_time_ms ();       /* Start over. */
          bartime = barlength;
        }

      /* Clear screen. */
      grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height);

      /* Border. */
      grub_video_fill_rect (framecolor,
                            bar_frame.x - 1, bar_frame.y - 1,
                            bar_frame.width + 2, bar_frame.height + 2);

      /* Bar background. */
      int barwidth = bar_frame.width * bartime / barlength;
      grub_video_fill_rect (barbgcolor, bar_frame.x + barwidth,
                            bar_frame.y, bar_frame.width - barwidth,
                            bar_frame.height);
      /* Bar foreground. */
      grub_video_fill_rect (barcolor, bar_frame.x, bar_frame.y,
                            barwidth, bar_frame.height);
      grub_video_swap_buffers ();
      frame_count++;
    }

  grub_uint32_t duration = grub_get_time_ms () - start_time;
  grub_uint32_t fps_x10 = 10 * 1000 * frame_count / duration;

  if (grub_checkkey () != -1)
    grub_getkey ();             /* Eat the keypress, if there was one. */
  grub_video_restore ();
  grub_printf ("Average frame rate: %d.%d fps\n", fps_x10 / 10, fps_x10 % 10);
}


/**
 * Test double buffering.
 */
static void
doublebuf_test (struct videotest_options *vt_opts)
{
  int mode_type = GRUB_VIDEO_MODE_TYPE_RGB;
  if (vt_opts->double_buffering)
    mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED;
  if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE)
    return;

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);

  grub_video_color_t red = grub_video_map_rgb (255, 50, 50);
  grub_video_color_t yellow = grub_video_map_rgb (255, 255, 0);
  grub_video_color_t green = grub_video_map_rgb (20, 255, 20);
  grub_video_color_t blue = grub_video_map_rgb (50, 50, 255);
  grub_video_color_t black = grub_video_map_rgb (0, 0, 0);
  grub_video_color_t bgcolor = grub_video_map_rgb (255, 255, 255);

  grub_video_fill_rect (bgcolor, 0, 0, 640, 480);
  grub_video_fill_rect (red, 100, 100, 200, 200);
  grub_video_swap_buffers ();
  grub_getkey ();

  grub_video_fill_rect (bgcolor, 0, 0, 640, 480);
  grub_video_fill_rect (yellow, 120, 120, 200, 200);
  grub_video_swap_buffers ();
  grub_getkey ();

  grub_video_fill_rect (bgcolor, 0, 0, 640, 480);
  grub_video_fill_rect (green, 140, 140, 200, 200);
  grub_video_swap_buffers ();
  grub_getkey ();

  grub_video_fill_rect (bgcolor, 0, 0, 640, 480);
  grub_video_fill_rect (blue, 160, 160, 200, 200);
  grub_video_swap_buffers ();
  grub_getkey ();

  grub_video_fill_rect (bgcolor, 0, 0, 640, 480);
  grub_video_fill_rect (black, 180, 180, 200, 200);
  grub_video_swap_buffers ();
  grub_getkey ();

  grub_video_restore ();
}


/**
 * Test text rendering.
 */
static void
text_test (struct videotest_options *vt_opts)
{
  grub_video_color_t color;
  const char *s;
  int view_x;
  int view_y;
  int view_width;
  int view_height;
  int xpos;
  int ypos;
  int i;
  grub_font_t font1;
  grub_font_t font2;
  grub_font_t font3;

  int mode_type = GRUB_VIDEO_MODE_TYPE_RGB;
  if (vt_opts->double_buffering)
    mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED;
  if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE)
    return;

  grub_video_get_viewport ((unsigned int *) &view_x,
                           (unsigned int *) &view_y,
                           (unsigned int *) &view_width,
                           (unsigned int *) &view_height);
  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);

  if (!(font1 = grub_font_get ("New Century Schoolbook 24"))
      || !(font2 = grub_font_get ("Helvetica Bold 14"))
      || !(font3 = grub_font_get ("Helvetica 10")))
    {
      grub_video_restore ();
      grub_error (GRUB_ERR_BAD_FONT, "No font loaded.");
      return;
    }

  color = grub_video_map_rgb (0, 0, 0);
  grub_video_fill_rect (color, 0, 0, view_width, view_height);

  color = grub_video_map_rgb (255, 0, 0);
  grub_video_fill_rect (color, 0, 0, 100, 100);

  color = grub_video_map_rgb (0, 255, 255);
  grub_video_fill_rect (color, 100, 100, 100, 100);

  color = grub_video_map_rgb (255, 255, 255);

  xpos = 10;
  ypos = 30;
  s = "Hello, World!";
  for (i = 0; i < 40; i++)
    {
      if (xpos + grub_font_get_string_width (font1, s) >= view_width)
        {
          /* The string will wrap; go to the beginning of the next line. */
          xpos = 10;
          ypos += (grub_font_get_descent (font1) 
                   + grub_font_get_ascent (font1));
        }
      grub_video_draw_string ("Hello, World!",
                              font1, grub_video_map_rgb (255, 255, 0), 
                              view_x + xpos, view_y + ypos);

      xpos += grub_font_get_string_width (font1, s);
    }

  xpos = 300;
  ypos = 450;
  grub_video_draw_string (grub_font_get_name (font1),
                          font1, grub_video_map_rgb (255, 255, 255),
                          view_x + xpos, view_y + ypos);
  ypos += grub_font_get_descent (font1) + grub_font_get_ascent (font2) + 2;
  grub_video_draw_string (grub_font_get_name (font2),
                          font2, grub_video_map_rgb (255, 255, 255),
                          view_x + xpos, view_y + ypos);
  ypos += grub_font_get_descent (font2) + grub_font_get_ascent (font3) + 2;
  grub_video_draw_string (grub_font_get_name (font3),
                          font3, grub_video_map_rgb (255, 255, 255),
                          view_x + xpos, view_y + ypos);

  grub_video_swap_buffers ();
  grub_getkey ();
  grub_video_restore ();
  grub_errno = GRUB_ERR_NONE;
}


/**
 * Test bitmap scaling.
 */
static void
scale_test (struct videotest_options *vt_opts)
{
  int mode_type = GRUB_VIDEO_MODE_TYPE_RGB;
  if (vt_opts->double_buffering)
    mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED;
  if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE)
    return;

  grub_errno = GRUB_ERR_NONE;
  grub_video_rect_t view;
  grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y,
                           (unsigned *) &view.width,
                           (unsigned *) &view.height);

  grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY);

  grub_font_t font;
  if (!(font = grub_font_get ("Helvetica 10")))
    {
      grub_video_restore ();
      grub_error (GRUB_ERR_BAD_FONT, "No font loaded.");
      return;
    }

  grub_video_color_t color;

  int text_y = 0;
  int text_height = 25;

  color = grub_video_map_rgb (44, 44, 200);
  grub_video_fill_rect (color, view.x, view.y, view.width, view.height);
  color = grub_video_map_rgb (255, 255, 255);

  grub_video_draw_string ("Loading image", 
                          font, color, 10, text_y += text_height);

  enum grub_video_bitmap_scale_method scale_method = 
    GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST;
  const char *bitmap_name = "/boot/grub/themes/winter/without-leaves.png";
  struct grub_video_bitmap *bitmap;
  grub_video_bitmap_load (&bitmap, bitmap_name);
  if (grub_errno != GRUB_ERR_NONE)
    {
      grub_video_draw_string ("Error loading bitmap", 
                              font, color, 10, text_y += text_height);
    }
  else 
    {
      grub_video_draw_string ("Original image", 
                              font, color, 10, text_y += text_height);
      grub_video_blit_bitmap (bitmap, GRUB_VIDEO_BLIT_BLEND,
                              400, text_y - 10,
                              0, 0, grub_video_bitmap_get_width (bitmap),
                              grub_video_bitmap_get_height (bitmap));

      struct grub_video_bitmap *bitmap2;
      if (grub_video_bitmap_create_scaled (&bitmap2, 40, 40,
                                              bitmap,
                                              scale_method)
          != GRUB_ERR_NONE)
        {
          grub_video_draw_string ("Error scaling down bitmap", 
                                  font, color, 10, text_y += text_height);
        }
      else
        {
          grub_video_draw_string ("Scaled down version", 
                                  font, color, 10, text_y += text_height);
          grub_video_blit_bitmap (bitmap2, GRUB_VIDEO_BLIT_BLEND,
                                  400, text_y + 100,
                                  0, 0, grub_video_bitmap_get_width (bitmap2),
                                  grub_video_bitmap_get_height (bitmap2));
          grub_video_bitmap_destroy (bitmap2);
        }

      struct grub_video_bitmap *bitmap3;
      if (grub_video_bitmap_create_scaled (&bitmap3, 500, 300, bitmap,
                                           scale_method)
          != GRUB_ERR_NONE)
        {
          grub_video_draw_string ("Error scaling up bitmap", 
                                  font, color, 10, text_y += text_height);
        }
      else
        {
          grub_video_draw_string ("Scaled up version", 
                                  font, color, 10, text_y += text_height);
          grub_video_blit_bitmap (bitmap3, GRUB_VIDEO_BLIT_BLEND,
                                  400, text_y + 50,
                                  0, 0, grub_video_bitmap_get_width (bitmap3),
                                  grub_video_bitmap_get_height (bitmap3));
          grub_video_bitmap_destroy (bitmap3);
        }
    }

  grub_video_swap_buffers ();
  grub_getkey ();
  grub_video_bitmap_destroy (bitmap);
  grub_video_restore ();
}


/** Print a list of the available tests. */
static void list_tests (void);

/**
 * Video test command.  Takes an argument specifying the test to run.
 */
static grub_err_t
grub_cmd_videotest (struct grub_arg_list *state, int argc, char **args)
{
  int i;
  struct videotest_options vt_opts;
  /* Pointer to the test function. */
  void (*test_func) (struct videotest_options *) = NULL;

  vt_opts.test_time =
    state[ARGINDEX_TEST_TIME].set
    ? grub_strtoul (state[ARGINDEX_TEST_TIME].arg, 0, 0) : 0;
  vt_opts.double_buffering = state[ARGINDEX_DOUBLE_BUF].set;

  /* Parse command line arguments to determine the test to run. */
  for (i = 0; i < argc; i++)
    {
      char *arg = args[i];
      if (grub_strcmp (arg, "list") == 0)
        {
          list_tests ();
          return GRUB_ERR_NONE;
        }
      else if (grub_strcmp (arg, "basic") == 0)
        {
          test_func = basic_video_test;
        }
      else if (grub_strcmp (arg, "bench") == 0)
        {
          test_func = benchmark_test;
        }
      else if (grub_strcmp (arg, "bitmaps") == 0)
        {
          test_func = bitmap_demo;
        }
      else if (grub_strcmp (arg, "clock") == 0)
        {
          test_func = clock_test;
        }
      else if (grub_strcmp (arg, "doublebuf") == 0)
        {
          test_func = doublebuf_test;
        }
      else if (grub_strcmp (arg, "text") == 0)
        {
          test_func = text_test;
        }
      else if (grub_strcmp (arg, "scale") == 0)
        {
          test_func = scale_test;
        }
      else
        {
          grub_printf ("Error: Unknown test `%s'\n", arg);
          grub_errno = GRUB_ERR_BAD_ARGUMENT;
          return grub_errno;
        }
    }

  if (test_func == NULL)
    {
      grub_printf ("Usage: videotest TESTNAME    Run a test.\n");
      grub_printf ("       videotest list        List available tests.\n");
      grub_printf ("\n");
      list_tests ();
    }
  else
    {
      test_func (&vt_opts);
    }

  return grub_errno;
}

static void
list_tests (void)
{
  grub_printf ("Available tests\n");
  grub_printf ("===============\n");
  grub_printf ("basic       Basic video test with filled rectangles,\n");
  grub_printf ("            offscreen rendering targets, some text.\n");
  grub_printf ("bench       Run a performance benchmark.\n");
  grub_printf ("bitmaps     Test generating and blitting bitmaps.\n");
  grub_printf ("clock       Test time functions w/ animated progress bar.\n");
  grub_printf ("doublebuf   Test double buffering.\n");
  grub_printf ("text        Test text rendering.\n");
  grub_printf ("scale       Test image scaling.\n");
  grub_printf ("\n");
}


GRUB_MOD_INIT (videotest)
{
  grub_register_command ("videotest",
                         grub_cmd_videotest,
                         GRUB_COMMAND_FLAG_BOTH,
                         "videotest TEST",
                         "Run the specified video subsystem test.",
                         arg_options);
}

GRUB_MOD_FINI (videotest)
{
  grub_unregister_command ("videotest");
}
