# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080705175913-xmp2j688zzymz798 # target_branch: ../../repo/trunk-clean/ # testament_sha1: f20a1f4db0c7558275043ca686fb96b6b43d1947 # timestamp: 2008-07-05 11:47:02 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080704180626-bbrj490ldahovosg # # Begin patch === modified file '.bzrignore' --- .bzrignore 2008-07-03 04:15:16 +0000 +++ .bzrignore 2008-07-03 14:08:39 +0000 @@ -6,3 +6,5 @@ ./conf/powerpc-ieee1275.mk ./conf/sparc64-ieee1275.mk ./build* +./util/fonttool/build +./util/fonttool/fonttool.iws === modified file 'ChangeLog' --- ChangeLog 2008-07-04 02:26:10 +0000 +++ ChangeLog 2008-07-04 19:43:25 +0000 @@ -1,3 +1,70 @@ +2008-07-04 Colin D Bennett + + High resolution timer support. Implemented for i386 CPU using TSC. + Extracted generic grub_millisleep() so it's linked in only as needed. + This requires a Pentium compatible CPU; currently the code does not + check for this (so it will fail on 386 and 486 machines). + + * conf/i386-efi.rmk: Added TSC high resolution time module, link with + generic grub_millisleep() function. + + * conf/i386-pc.rmk: Likewise. + + * conf/sparc64-ieee1275.rmk: Add kern/generic/millisleep.c and + kern/generic/get_time_ms.c to kernel, to use generic time functions. + + * conf/powerpc-ieee1275.rmk: Add kern/generic/millisleep.c to kernel, + to use generic grub_millisleep() function. + + * conf/i386-linuxbios.rmk: Added kern/generic/get_time_ms.c to the + kernel. + + * kern/generic/get_time_ms.c (grub_get_time_ms): New file. Platform + independent implementation of grub_get_time_ms() using the RTC that + can be linked into a platform's kernel when it does not implement its + own specialized grub_get_time_ms() function. + + * kern/generic/millisleep.c (grub_millisleep): New file. Extracted + from grub_millisleep_generic() in kern/misc.c and renamed. Changed it + to use grub_get_time_ms() instead of grub_get_rtc() for better + precision on when high resolution time is available. + + * kern/misc.c (grub_millisleep_generic): Deleted. Moved to + kern/generic/millisleep.c so that it is only included in the kernel + image when a platform does not define a specialized version. + + * commands/sleep.c (grub_interruptible_millisleep): Uses + grub_get_time_ms() instead of grub_get_rtc() to stay in sync with + grub_millisleep() from kern/generic/millisleep.c. + + * include/grub/i386/tsc.h (grub_get_tsc): New file. Inline function + grub_get_tsc() uses x86 RDTSC instruction (available on Pentium+ CPUs) + to read the counter value for the TSC. + (grub_tsc_calibrate): Declare this function for grub_machine_init(). + + * kern/i386/tsc.c (grub_get_time_ms): x86 TSC support providing a high + resolution clock. + (grub_tsc_calibrate): New function to calibrate the TSC using RTC. + + * include/grub/time.h (grub_get_time_ms): Added grub_get_time_ms() + function to return the current time in millseconds since the epoch. + This supports higher resolution time than grub_get_rtc() on some + platforms such as i386-pc, where the RTC has only about 1/18 s + precision but a higher precision timer such as the TSC is available. + + * kern/i386/efi/init.c (grub_millisleep): Deleted. Don't define + grub_millisleep() -- it just called grub_millisleep_generic() but now + it is linked to kern/generic/millisleep.c for the implementation. + + * kern/sparc64/ieee1275/init.c (grub_millisleep): Deleted. + + * kern/i386/pc/init.c (grub_machine_init): Call grub_tsc_calibrate(). + (grub_millisleep): Deleted. + + * kern/ieee1275/init.c (grub_millisleep): Deleted. + (grub_get_rtc): Now calls grub_get_time_ms(), which does the real + work. + 2008-07-04 Pavel Roskin * kern/i386/linuxbios/init.c (grub_machine_init): Cast addr to === modified file 'Makefile.in' --- Makefile.in 2008-07-02 18:03:23 +0000 +++ Makefile.in 2008-07-04 21:40:36 +0000 @@ -85,6 +85,39 @@ YACC = @YACC@ UNIFONT_HEX = @UNIFONT_HEX@ +### Pretty output control ### +# Set up compiler and linker commands that either is quiet (does not print +# the command line being executed) or verbose (print the command line). +_CC := $(CC) +_TARGET_CC := $(TARGET_CC) +_STRIP := $(STRIP) +_GENMODSRC := sh $(srcdir)/genmodsrc.sh +ifeq ($(V),1) + override V_PREFIX := + override CC = $(_CC) + override TARGET_CC = $(_CC) + override STRIP = $(_STRIP) + override GENMODSRC = $(_GENMODSRC) + override INFO_GENCMDLIST = + override INFO_GENFSLIST = + override INFO_GENPARTMAPLIST = + override INFO_GEN_FINAL_COMMAND_LIST = + override INFO_GEN_FINAL_FS_LIST = + override INFO_GEN_FINAL_PARTMAP_LIST = +else + override V_PREFIX := @ + override CC = @echo "COMPILE $<"; $(_CC) + override TARGET_CC = @echo "COMPILE(TARGET) $<"; $(_TARGET_CC) + override STRIP = @echo "STRIP $@"; $(_STRIP) + override GENMODSRC = @echo "GENMODSRC $@"; $(_GENMODSRC) + override INFO_GENCMDLIST = @echo "GENCMDLIST $@" + override INFO_GENFSLIST = @echo "GENFSLIST $@" + override INFO_GENPARTMAPLIST = @echo "GENPARTMAPLIST $@" + override INFO_GEN_FINAL_COMMAND_LIST = @echo "GENCMDLIST[final] $@" + override INFO_GEN_FINAL_FS_LIST = @echo "GENFSLIST[final] $@" + override INFO_GEN_FINAL_PARTMAP_LIST = @echo "GENPARTMAPLIST[final] $@" +endif + # Options. enable_grub_emu = @enable_grub_emu@ enable_grub_fstest = @enable_grub_fstest@ @@ -131,13 +164,16 @@ || (rm -f $@; exit 1) command.lst: $(COMMANDFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_COMMAND_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ fs.lst: $(FSFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_FS_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ partmap.lst: $(PARTMAPFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_PARTMAP_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ ifeq (, $(UNIFONT_HEX)) else === modified file 'commands/i386/pc/vbeinfo.c' --- commands/i386/pc/vbeinfo.c 2007-07-21 22:32:33 +0000 +++ commands/i386/pc/vbeinfo.c 2008-07-03 13:40:06 +0000 @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -67,6 +68,11 @@ grub_memcpy (saved_video_mode_list, video_mode_list, video_mode_list_size); + grub_printf ("Legend: P=Packed pixel, D=Direct color, " + "mask/pos=R/G/B/reserved\n"); + + int row = 3; + /* Walk through all video modes listed. */ for (p = saved_video_mode_list; *p != 0xFFFF; p++) { @@ -103,10 +109,10 @@ switch (mode_info_tmp.memory_model) { case 0x04: - memory_model = "Packed Pixel"; + memory_model = "Packed"; break; case 0x06: - memory_model = "Direct Color"; + memory_model = "Direct"; break; default: @@ -116,12 +122,31 @@ if (! memory_model) continue; - grub_printf ("0x%03x: %d x %d x %d bpp (%s)\n", - mode, + grub_printf ("0x%03x: %4dx%4dx%2d %s", + mode, mode_info_tmp.x_resolution, mode_info_tmp.y_resolution, mode_info_tmp.bits_per_pixel, - memory_model); + memory_model); + if (memory_model[0] == 'D') + grub_printf (" mask: %d/%d/%d/%d pos: %d/%d/%d/%d", + mode_info_tmp.red_mask_size, + mode_info_tmp.green_mask_size, + mode_info_tmp.blue_mask_size, + mode_info_tmp.rsvd_mask_size, + mode_info_tmp.red_field_position, + mode_info_tmp.green_field_position, + mode_info_tmp.blue_field_position, + mode_info_tmp.rsvd_field_position); + grub_printf ("\n"); + + if (++row >= 25) + { + grub_printf ("-- More --"); + grub_getkey (); + grub_printf ("\r \r"); + row = 0; + } } grub_free (saved_video_mode_list); === modified file 'commands/sleep.c' --- commands/sleep.c 2008-05-16 20:55:29 +0000 +++ commands/sleep.c 2008-07-04 16:55:48 +0000 @@ -43,15 +43,15 @@ grub_printf ("%d ", n); } -/* Based on grub_millisleep() from kern/misc.c. */ +/* Based on grub_millisleep() from kern/generic/millisleep.c. */ static int grub_interruptible_millisleep (grub_uint32_t ms) { - grub_uint32_t end_at; - - end_at = grub_get_rtc () + grub_div_roundup (ms * GRUB_TICKS_PER_SECOND, 1000); - - while (grub_get_rtc () < end_at) + grub_uint64_t start; + + start = grub_get_time_ms (); + + while (grub_get_time_ms () - start < ms) if (grub_checkkey () >= 0 && GRUB_TERM_ASCII_CHAR (grub_getkey ()) == GRUB_TERM_ESC) return 1; === modified file 'commands/videotest.c' --- commands/videotest.c 2007-07-21 22:32:33 +0000 +++ commands/videotest.c 2008-07-03 14:38:57 +0000 @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -26,15 +27,51 @@ #include #include #include - -static grub_err_t -grub_cmd_videotest (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ +#include +#include +#include /* 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; +}; + +/* A 2D rectangle type. + * This could be worth integrating into the video API if it proves useful.*/ +struct grub_video_rect +{ + /* These are signed because if there are unsigned it causes Bad Things + * to happen when arithmetic and comparisions involving signed types is + * done. Important signed types include offsets from absolute locations. */ + int x; + int y; + int width; + int height; +}; +typedef struct grub_video_rect grub_video_rect_t; + +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_errno; + return; grub_getkey (); @@ -44,7 +81,8 @@ unsigned int width; unsigned int height; int i; - struct grub_font_glyph glyph; + grub_font_t font; + struct grub_font_glyph *glyph; struct grub_video_render_target *text_layer; grub_video_color_t palette[16]; @@ -65,8 +103,15 @@ color = grub_video_map_rgb (0, 255, 255); grub_video_fill_rect (color, 100, 100, 100, 100); - grub_font_get_glyph ('*', &glyph); - grub_video_blit_glyph (&glyph, color, 200 ,0); + 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); @@ -77,18 +122,18 @@ color = grub_video_map_rgb (255, 255, 255); - grub_font_get_glyph ('A', &glyph); - grub_video_blit_glyph (&glyph, color, 16, 16); - grub_font_get_glyph ('B', &glyph); - grub_video_blit_glyph (&glyph, color, 16 * 2, 16); + 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); - grub_font_get_glyph ('*', &glyph); + 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_blit_glyph (glyph, color, 16 + i * 16, 32); } grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); @@ -101,30 +146,1075 @@ 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 (); - - for (i = 0; i < 16; i++) - grub_printf("color %d: %08x\n", i, palette[i]); - - grub_errno = GRUB_ERR_NONE; + 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 (32), 0}, + {640, 480, MODE_TYPE_RGB (32), 0}, + {1024, 768, MODE_TYPE_RGB (32), 0}, + {1024, 768, MODE_TYPE_RGB (32), 1}, + {1024, 768, MODE_TYPE_RGB (24), 0}, + {1024, 768, MODE_TYPE_RGB (24), 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 * 100 to achieve two decimal places. */ + /* 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 100 + +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_uint32_t start_time; + grub_uint32_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_rtc (); + desired_stop_time = start_time + vt_opts->test_time * GRUB_TICKS_PER_SECOND; + + while (grub_get_rtc () < 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_rtc () - start_time; + if (duration == 0) + { + result->fill_fps = 0; + } + else + { + result->fill_fps = + (BENCHMARK_RESULT_FPS_SCALE * GRUB_TICKS_PER_SECOND + * 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_rtc (); + desired_stop_time = start_time + vt_opts->test_time * GRUB_TICKS_PER_SECOND; + + int cur = 0; /* Which bitmap to draw this frame. */ + while (grub_get_rtc () < 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_rtc () - start_time; + if (duration == 0) + { + result->blit_fps = 0; + } + else + { + result->blit_fps = + (BENCHMARK_RESULT_FPS_SCALE * GRUB_TICKS_PER_SECOND + * 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_rtc (); + desired_stop_time = start_time + vt_opts->test_time * GRUB_TICKS_PER_SECOND; + + + 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_rtc () < 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_rtc () - start_time; + if (duration == 0) + { + result->blend_fps = 0; + } + else + { + result->blend_fps = + (BENCHMARK_RESULT_FPS_SCALE * GRUB_TICKS_PER_SECOND + * 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++) + { + do_benchmark (&benchmark_configs[i], &results[i], vt_opts); + } + + /* 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.%02d %4d.%02d %4d.%02d\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/images/wallpaper.tga"; + 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; } -GRUB_MOD_INIT(videotest) +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 video subsystem", - 0); + "videotest TEST", + "Run the specified video subsystem test.", + arg_options); } -GRUB_MOD_FINI(videotest) +GRUB_MOD_FINI (videotest) { grub_unregister_command ("videotest"); } === modified file 'conf/common.rmk' --- conf/common.rmk 2008-07-02 06:20:11 +0000 +++ conf/common.rmk 2008-07-05 17:04:34 +0000 @@ -287,6 +287,7 @@ cmp.mod cat.mod help.mod font.mod search.mod \ loopback.mod fs_uuid.mod configfile.mod echo.mod \ terminfo.mod test.mod blocklist.mod hexdump.mod \ + protomenu.mod \ read.mod sleep.mod loadenv.mod # For hello.mod. @@ -330,7 +331,7 @@ help_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) @@ -374,6 +375,11 @@ hexdump_mod_CFLAGS = $(COMMON_CFLAGS) hexdump_mod_LDFLAGS = $(COMMON_LDFLAGS) +# For protomenu.mod. +protomenu_mod_SOURCES = protomenu/protomenu.c +protomenu_mod_CFLAGS = $(COMMON_CFLAGS) +protomenu_mod_LDFLAGS = $(COMMON_LDFLAGS) + # For read.mod. read_mod_SOURCES = commands/read.c read_mod_CFLAGS = $(COMMON_CFLAGS) === modified file 'conf/i386-efi.rmk' --- conf/i386-efi.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-efi.rmk 2008-07-03 04:19:16 +0000 @@ -84,7 +84,9 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/efi/init.c kern/parser.c kern/partition.c \ kern/env.c symlist.c kern/efi/efi.c kern/efi/init.c kern/efi/mm.c \ - term/efi/console.c disk/efi/efidisk.c + term/efi/console.c disk/efi/efidisk.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c kernel_mod_HEADERS = arg.h boot.h cache.h device.h disk.h dl.h elf.h elfload.h \ env.h err.h file.h fs.h kernel.h loader.h misc.h mm.h net.h parser.h \ partition.h pc_partition.h rescue.h symbol.h term.h time.h types.h \ === modified file 'conf/i386-linuxbios.rmk' --- conf/i386-linuxbios.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-linuxbios.rmk 2008-07-04 16:55:48 +0000 @@ -18,6 +18,7 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/parser.c kern/partition.c \ kern/env.c \ + kern/generic/get_time_ms.c \ term/i386/pc/console.c \ term/i386/pc/at_keyboard.c term/i386/pc/vga_text.c \ symlist.c === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-pc.rmk 2008-07-03 14:27:43 +0000 @@ -43,6 +43,8 @@ kern/disk.c kern/dl.c kern/file.c kern/fs.c kern/err.c \ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/pc/init.c kern/parser.c kern/partition.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c \ kern/env.c \ term/i386/pc/console.c \ symlist.c @@ -256,7 +258,10 @@ videotest_mod_LDFLAGS = $(COMMON_LDFLAGS) # For bitmap.mod -bitmap_mod_SOURCES = video/bitmap.c +bitmap_mod_SOURCES = video/bitmap.c \ + video/bitmap_scale_nn.c \ + video/bitmap_scale_bilinear.c \ + video/bitmap_scale.c bitmap_mod_CFLAGS = $(COMMON_CFLAGS) bitmap_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'conf/powerpc-ieee1275.rmk' --- conf/powerpc-ieee1275.rmk 2008-06-19 04:14:16 +0000 +++ conf/powerpc-ieee1275.rmk 2008-07-03 04:19:16 +0000 @@ -85,6 +85,7 @@ kern/ieee1275/init.c term/ieee1275/ofconsole.c \ kern/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/parser.c kern/partition.c kern/env.c kern/powerpc/dl.c \ + kern/generic/millisleep.c \ symlist.c kern/powerpc/cache.S kernel_elf_HEADERS = grub/powerpc/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) === modified file 'conf/sparc64-ieee1275.rmk' --- conf/sparc64-ieee1275.rmk 2008-06-19 00:04:59 +0000 +++ conf/sparc64-ieee1275.rmk 2008-07-03 14:16:34 +0000 @@ -73,6 +73,7 @@ kern/rescue.c kern/term.c term/ieee1275/ofconsole.c \ kern/sparc64/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/partition.c kern/env.c kern/sparc64/dl.c symlist.c \ + kern/generic/millisleep.c kern/generic/get_time_ms.c \ kern/sparc64/cache.S kern/parser.c kernel_elf_HEADERS = grub/sparc64/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) @@ -195,7 +196,7 @@ cat_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) === added file 'font/font.c' --- font/font.c 1970-01-01 00:00:00 +0000 +++ font/font.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,92 @@ +/* font.c - Font functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 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 . + */ + +#include +#include +#include +#include +#include +#include +#include + +grub_font_t +grub_font_get (const char *font_name) +{ + struct font_node *node; + + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + if (grub_strcmp (font->name, font_name) == 0) + return font; + } + + /* If no font by that name is found, return the first font in the list + * as a fallback. */ + return grub_font_list->value; +} + +const char * +grub_font_get_name (grub_font_t font) +{ + return font->name; +} + +int +grub_font_get_max_char_width (grub_font_t font) +{ + return font->max_char_width; +} + +int +grub_font_get_max_char_height (grub_font_t font) +{ + return font->max_char_height; +} + +int +grub_font_get_ascent (grub_font_t font) +{ + return font->ascent; +} + +int +grub_font_get_descent (grub_font_t font) +{ + return font->descent; +} + +int +grub_font_get_string_width (grub_font_t font, const char *str) +{ + int i; + int width; + struct grub_font_glyph *glyph; + grub_size_t len; + + len = grub_strlen (str); + + for (i = 0, width = 0; i < len; i++) + { + glyph = grub_font_get_glyph (font, str[i]); + width += glyph->device_width; + } + + return width; +} === added file 'font/font_cmd.c' --- font/font_cmd.c 1970-01-01 00:00:00 +0000 +++ font/font_cmd.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,75 @@ +/* font_cmd.c - Font command definition. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,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 . + */ + +#include +#include + +static grub_err_t +loadfont_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc, + char **args) +{ + if (argc == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); + + while (argc--) + if (grub_font_load (*args++) != 0) + return GRUB_ERR_BAD_FONT; + + return GRUB_ERR_NONE; +} + +static grub_err_t +lsfonts_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + struct font_node *node; + + grub_printf ("Loaded fonts:\n"); + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + grub_printf ("%s\n", font->name); + } + + return GRUB_ERR_NONE; +} + +GRUB_MOD_INIT(font_manager) +{ + grub_font_loader_init (); + + grub_register_command ("loadfont", loadfont_command, GRUB_COMMAND_FLAG_BOTH, + "loadfont FILE...", + "Specify one or more font files to load.", 0); + + grub_register_command ("lsfonts", lsfonts_command, GRUB_COMMAND_FLAG_BOTH, + "lsfonts", + "List the loaded fonts.", 0); +} + +GRUB_MOD_FINI(font_manager) +{ + /* Should this free fonts, unknown_glyph, etc.? Freeing fonts could + * be a Bad Thing if there are still references to any of them. */ + + grub_unregister_command ("loadfont"); +} + === added file 'font/loader.c' --- font/loader.c 1970-01-01 00:00:00 +0000 +++ font/loader.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,696 @@ +/* loader.c - Functions to handle loading fonts and glyphs from files. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 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 . + */ + +#include +#include +//#include +//#include +#include +#include +#include + +/* Definition of font registry. */ +struct font_node *grub_font_list; + +static int +register_font (grub_font_t font); + +static void +free_font (grub_font_t font); + +static void +remove_font (grub_font_t font); + +struct font_file_section +{ + grub_file_t file; /* The file this section is in. */ + char name[4]; /* FOURCC name of the section. */ + grub_uint32_t length; /* Length of the section contents. */ + int eof; /* Set by open_section() on EOF. */ +}; + +/* Font file format constants. */ +static const char pff2_magic[4] = { 'P', 'F', 'F', '2' }; +static const char section_names_file[4] = { 'F', 'I', 'L', 'E' }; +static const char section_names_font_name[4] = { 'N', 'A', 'M', 'E' }; +static const char section_names_max_char_width[4] = { 'M', 'A', 'X', 'W' }; +static const char section_names_max_char_height[4] = { 'M', 'A', 'X', 'H' }; +static const char section_names_ascent[4] = { 'A', 'S', 'C', 'E' }; +static const char section_names_descent[4] = { 'D', 'E', 'S', 'C' }; +static const char section_names_char_index[4] = { 'C', 'H', 'I', 'X' }; +static const char section_names_data[4] = { 'D', 'A', 'T', 'A' }; + +/* Replace unknown glyphs with a rounded question mark. */ +static grub_uint8_t unknown_glyph_bitmap[] = +{ + /* 76543210 */ + 0x7C, /* ooooo */ + 0x82, /* o o */ + 0xBA, /* o ooo o */ + 0xAA, /* o o o o */ + 0xAA, /* o o o o */ + 0x8A, /* o o o */ + 0x9A, /* o oo o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x7C, /* ooooo */ + 0x00 /* */ +}; + +static struct grub_font_glyph *unknown_glyph; + +void +grub_font_loader_init (void) +{ + unknown_glyph = grub_malloc(sizeof(struct grub_font_glyph) + + sizeof(unknown_glyph_bitmap)); + if (! unknown_glyph) + return; + + unknown_glyph->width = 8; + unknown_glyph->height = 16; + unknown_glyph->offset_x = 0; + unknown_glyph->offset_y = 0; + unknown_glyph->device_width = 8; + grub_memcpy(unknown_glyph->bitmap, + unknown_glyph_bitmap, sizeof(unknown_glyph_bitmap)); +} + +/* + * Open the next section in the file. + * + * On success, the section name is stored in section->name and the length in + * section->length, and 0 is returned. On failure, 1 is returned and + * grub_errno is set approriately with an error message. + * + * If 1 is returned due to being at the end of the file, then section->eof is + * set to 1; otherwise, section->eof is set to 0. + */ +static int +open_section (grub_file_t file, struct font_file_section *section) +{ + grub_ssize_t retval; + grub_uint32_t raw_length; + + section->file = file; + section->eof = 0; + + /* Read the FOURCC section name. */ + retval = grub_file_read (file, section->name, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section name"); + return 1; /* Read error. */ + } + + /* Read the big-endian 32-bit section length. */ + retval = grub_file_read (file, (char *) &raw_length, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section length"); + return 1; /* Read error. */ + } + + /* Convert byte-order and store in *length. */ + section->length = grub_be_to_cpu32 (raw_length); + + return 0; +} + +/* Size in bytes of each character index (CHIX section) + * entry in the font file. */ +#define FONT_CHAR_INDEX_ENTRY_SIZE (4 + 1 + 4) + +/* + * Load the character index (CHIX) section contents from the font file. This + * presumes that the position of FILE is positioned immediately after the + * section length for the CHIX section (i.e., at the start of the section + * contents). Returns 0 upon success, nonzero for failure (in which case + * grub_errno is set appropriately). + */ +static int +load_font_index (grub_file_t file, grub_uint32_t sect_length, struct + grub_font *font) +{ + unsigned i; + +#if FONT_DEBUG >= 2 + grub_printf("load_font_index(sect_length=%d)\n", sect_length); +#endif + + /* Sanity check: ensure section length is divisible by the entry size. */ + if (sect_length % FONT_CHAR_INDEX_ENTRY_SIZE != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: character index length %d " + "is not a multiple of the entry size %d", + sect_length, FONT_CHAR_INDEX_ENTRY_SIZE); + return 1; /* Invalid index section length. */ + } + + /* Calculate the number of characters. */ + font->num_chars = sect_length / FONT_CHAR_INDEX_ENTRY_SIZE; + + /* Allocate the character index array. */ + font->char_index = grub_malloc (font->num_chars + * sizeof (struct char_index_entry)); + if (!font->char_index) + return 1; /* Error allocating memory. */ + +#if FONT_DEBUG >= 2 + grub_printf("num_chars=%d)\n", font->num_chars); +#endif + + /* Load the character index data from the file. */ + for (i = 0; i < font->num_chars; i++) + { + struct char_index_entry *entry = &font->char_index[i]; + + /* Read code point value; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->code, 4) != 4) + return 1; + entry->code = grub_be_to_cpu32 (entry->code); + + /* Read storage flags byte. */ + if (grub_file_read (file, (char *) &entry->storage_flags, 1) != 1) + return 1; + + /* Read glyph data offset; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->offset, 4) != 4) + return 1; + entry->offset = grub_be_to_cpu32 (entry->offset); + + /* No glyph loaded. Will be loaded on demand and cached thereafter. */ + entry->glyph = 0; + +#if FONT_DEBUG >= 5 + if (i < 10) /* Print the 1st 10 characters. */ + grub_printf("c=%d o=%d\n", entry->code, entry->offset); +#endif + } + + return 0; /* Index loaded OK. */ +} + +/* + * Read the contents of the specified section as a string, which is + * allocated on the heap. Returns 0 if there is an error. + */ +static char * +read_section_as_string (struct font_file_section *section) +{ + char *str; + grub_ssize_t ret; + + str = grub_malloc (section->length + 1); + if (!str) + return 0; + + ret = grub_file_read (section->file, str, section->length); + if (ret < 0 || ret != (grub_ssize_t) section->length) + { + grub_free (str); + return 0; + } + + str[section->length] = '\0'; + return str; +} + +/* + * Read the contents of the current section as a 16-bit integer value, + * which is stored into *VALUE. Returns 0 upon success, nonzero upon failure. + */ +static int +read_section_as_short (struct font_file_section *section, grub_int16_t *value) +{ + grub_uint16_t raw_value; + + if (section->length != 2) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: section %c%c%c%c length " + "is %d but should be 2", + section->name[0], section->name[1], + section->name[2], section->name[3], + section->length); + return 1; /* An error occurred. */ + } + if (grub_file_read (section->file, (char *) &raw_value, 2) != 2) + return 1; /* An error occurred. */ + + *value = grub_be_to_cpu16 (raw_value); + return 0; /* Successfully read the value. */ +} + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int +grub_font_load (const char *filename) +{ + grub_file_t file = 0; + struct font_file_section section; + char magic[4]; + grub_font_t font = 0; + +#if FONT_DEBUG >= 1 + grub_printf("add_font(%s)\n", filename); +#endif + + file = grub_file_open (filename); + if (!file) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("file opened\n"); +#endif + + /* Read the FILE section. It indicates the file format. */ + if (open_section (file, §ion) != 0) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("opened FILE section\n"); +#endif + if (grub_memcmp (section.name, section_names_file, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: 1st section must be FILE"); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section name ok\n"); +#endif + if (section.length != 4) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error (file type ID length is %d " + "but should be 4)", section.length); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section length ok\n"); +#endif + /* Check the file format type code. */ + if (grub_file_read (file, magic, 4) != 4) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("read magic ok\n"); +#endif + + if (grub_memcmp (magic, pff2_magic, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, "Invalid font magic %x %x %x %x", + magic[0], magic[1], magic[2], magic[3]); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("compare magic ok\n"); +#endif + + /* Allocate the font object. */ + font = (grub_font_t) grub_malloc (sizeof (struct grub_font)); + if (!font) + goto fail; + + font->file = file; + font->name = 0; + font->ascent = 0; + font->descent = 0; + font->max_char_width = 0; + font->max_char_height = 0; + font->num_chars = 0; + font->char_index = 0; + +#if FONT_DEBUG >= 3 + grub_printf("allocate font ok; loading font info\n"); +#endif + + /* Load the font information. */ + while (1) + { + if (open_section (file, §ion) != 0) + { + if (section.eof) + break; /* Done reading the font file. */ + else + goto fail; + } + +#if FONT_DEBUG >= 2 + grub_printf("opened section %c%c%c%c ok\n", + section.name[0], section.name[1], + section.name[2], section.name[3]); +#endif + + if (grub_memcmp (section.name, section_names_font_name, 4) == 0) + { + font->name = read_section_as_string (§ion); + if (!font->name) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_width, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_width) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_height, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_height) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_ascent, 4) == 0) + { + if (read_section_as_short (§ion, &font->ascent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_descent, 4) == 0) + { + if (read_section_as_short (§ion, &font->descent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_char_index, 4) == 0) + { + /* Load the font index. */ + if (load_font_index (file, section.length, font) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_data, 4) == 0) + { + /* When the DATA section marker is reached, we stop reading. */ + break; + } + else + { + /* Unhandled section type, simply skip past it. */ +#if FONT_DEBUG >= 3 + grub_printf("Unhandled section type, skipping.\n"); +#endif + if ((int) grub_file_seek (file, + grub_file_tell (file) + section.length) + == -1) + goto fail; + } + } + + if (!font->name) + { + grub_printf ("Note: Font has no name.\n"); + font->name = grub_strdup ("Unknown"); + } + +#if FONT_DEBUG >= 1 + grub_printf ("Loaded font `%s'.\n" + "Ascent=%d Descent=%d MaxW=%d MaxH=%d Number of characters=%d.\n", + font->name, + font->ascent, font->descent, + font->max_char_width, font->max_char_height, + font->num_chars); +#endif + + if (font->max_char_width == 0 + || font->max_char_height == 0 + || font->num_chars == 0 + || font->char_index == 0 + || font->ascent == 0 + || font->descent == 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Invalid font file: missing some required data."); + goto fail; + } + + /* Add the font to the global font registry. */ + if (register_font (font) != 0) + goto fail; + + return 0; /* Font loaded ok. */ + +fail: + free_font (font); + return 1; /* Failed to load font. */ +} + +/* + * Read a 16-bit big-endian integer from FILE, convert it to native byte + * order, and store it in *VALUE. + * Returns 0 on success, 1 on failure. + */ +static int +read_be_uint16 (grub_file_t file, grub_uint16_t * value) +{ + if (grub_file_read (file, (char *) value, 2) != 2) + return 1; + *value = grub_be_to_cpu16 (*value); + return 0; +} + +static int +read_be_int16 (grub_file_t file, grub_int16_t * value) +{ + /* For the signed integer version, use the same code as for unsigned. */ + return read_be_uint16 (file, (grub_uint16_t *) value); +} + +/* + * Return a pointer to the character index entry for the glyph corresponding to + * the codepoint CODE in the font FONT. If not found, return zero. + */ +static struct char_index_entry * +find_glyph (const grub_font_t font, grub_uint32_t code) +{ + grub_uint32_t i; + grub_uint32_t len = font->num_chars; + struct char_index_entry *table = font->char_index; + + /* Do a linear search. */ + for (i = 0; i < len; i++) + { + if (table[i].code == code) + return &table[i]; + } + + return 0; /* No entry found for code point CODE. */ +} + +static struct grub_font_glyph * +grub_font_get_glyph_internal (grub_font_t font, grub_uint32_t code) +{ + struct char_index_entry *index_entry; + + index_entry = find_glyph (font, code); + if (index_entry) + { + struct grub_font_glyph *glyph = 0; + grub_uint16_t width; + grub_uint16_t height; + grub_int16_t xoff; + grub_int16_t yoff; + grub_int16_t dwidth; + int len; + + if (index_entry->glyph) + return index_entry->glyph; /* Return cached glyph. */ + + /* Make sure we can find glyphs for error messages. Push active + error message to error stack and reset error message. */ + grub_error_push (); + + grub_file_seek (font->file, index_entry->offset); + + /* Read the glyph width, height, and baseline. */ + if (read_be_uint16(font->file, &width) != 0 + || read_be_uint16(font->file, &height) != 0 + || read_be_int16(font->file, &xoff) != 0 + || read_be_int16(font->file, &yoff) != 0 + || read_be_int16(font->file, &dwidth) != 0) + { + //remove_font (font); + return 0; + } + + len = (width * height + 7) / 8; + glyph = grub_malloc (sizeof (struct grub_font_glyph) + len); + if (! glyph) + { + //remove_font (font); + return 0; + } + + glyph->font = font; + glyph->width = width; + glyph->height = height; + glyph->offset_x = xoff; + glyph->offset_y = yoff; + glyph->device_width = dwidth; + + /* Don't try to read empty bitmaps (e.g., space characters). */ + if (len != 0) + { + if (grub_file_read (font->file, (char *) glyph->bitmap, len) != len) + { + //remove_font (font); + return 0; + } + } + + /* Restore old error message. */ + grub_error_pop (); + + /* Cache the glyph. */ + index_entry->glyph = glyph; + + return glyph; /* Glyph loaded ok. */ + } + + return 0; +} + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph * +grub_font_get_glyph (grub_font_t font, grub_uint32_t code) +{ + struct grub_font_glyph *glyph; + glyph = grub_font_get_glyph_internal (font, code); + if (glyph == 0) + glyph = unknown_glyph; + return glyph; +} + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph * +grub_font_get_glyph_any (grub_uint32_t code) +{ + struct font_node *node; + /* Keep track of next node, in case there's an I/O error in + * grub_font_get_glyph() and the font is removed from the list. */ + struct font_node *next; + + for (node = grub_font_list; node; node = next) + { + grub_font_t font; + struct grub_font_glyph *glyph; + + font = node->value; + next = node->next; + + glyph = grub_font_get_glyph_internal (font, code); + if (glyph) + return glyph; + } + + /* Uggh... Glyph was not found in any font. */ + return unknown_glyph; /* Failed to load glyph. */ +} + +/* + * Free the memory used by a font. + * This should not be called if the font has been made available to + * users (once it is added to the global font list), since there would + * be the possibility of a dangling pointer. + */ +static void +free_font (grub_font_t font) +{ + if (font) + { + if (font->file) + grub_file_close (font->file); + if (font->name) + grub_free (font->name); + if (font->char_index) + grub_free (font->char_index); + grub_free (font); + } +} + +/* + * Add FONT to the global font registry. + * Returns 0 upon success, nonzero on failure (the font was not registered). + */ +static int +register_font (grub_font_t font) +{ + struct font_node *node = 0; + + node = grub_malloc (sizeof (struct font_node)); + if (!node) + return 1; /* Error. */ + + node->value = font; + node->next = grub_font_list; + grub_font_list = node; + + return 0; /* Success. */ +} + +/* + * Remove the font from the global font list. We don't actually free the + * font's memory since users could be holding references to the font. + */ +static void +remove_font (grub_font_t font) +{ + struct font_node **nextp, *cur; + + for (nextp = &grub_font_list, cur = *nextp; + cur; + nextp = &cur->next, cur = cur->next) + { + if (cur->value == font) + { + *nextp = cur->next; + + /* Free the node, but not the font itself. */ + grub_free (cur); + + return; + } + } +} + === removed file 'font/manager.c' --- font/manager.c 2008-02-10 11:04:38 +0000 +++ font/manager.c 1970-01-01 00:00:00 +0000 @@ -1,282 +0,0 @@ -/* - * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2005,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 . - */ - -#include -#include -#include -#include -#include -#include -#include - -struct entry -{ - grub_uint32_t code; - grub_uint32_t offset; -}; - -struct font -{ - struct font *next; - grub_file_t file; - grub_uint32_t num; - struct entry table[0]; -}; - -static struct font *font_list; - -/* Fill unknown glyph's with rounded question mark. */ -static grub_uint8_t unknown_glyph[16] = -{ /* 76543210 */ - 0x7C, /* ooooo */ - 0x82, /* o o */ - 0xBA, /* o ooo o */ - 0xAA, /* o o o o */ - 0xAA, /* o o o o */ - 0x8A, /* o o o */ - 0x9A, /* o oo o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x7C, /* ooooo */ - 0x00 /* */ -}; - -static int -add_font (const char *filename) -{ - grub_file_t file = 0; - char magic[4]; - grub_uint32_t num, i; - struct font *font = 0; - - file = grub_file_open (filename); - if (! file) - goto fail; - - if (grub_file_read (file, magic, 4) != 4) - goto fail; - - if (grub_memcmp (magic, GRUB_FONT_MAGIC, 4) != 0) - { - grub_error (GRUB_ERR_BAD_FONT, "invalid font magic"); - goto fail; - } - - if (grub_file_read (file, (char *) &num, 4) != 4) - goto fail; - - num = grub_le_to_cpu32 (num); - font = (struct font *) grub_malloc (sizeof (struct font) - + sizeof (struct entry) * num); - if (! font) - goto fail; - - font->file = file; - font->num = num; - - for (i = 0; i < num; i++) - { - grub_uint32_t code, offset; - - if (grub_file_read (file, (char *) &code, 4) != 4) - goto fail; - - if (grub_file_read (file, (char *) &offset, 4) != 4) - goto fail; - - font->table[i].code = grub_le_to_cpu32 (code); - font->table[i].offset = grub_le_to_cpu32 (offset); - } - - font->next = font_list; - font_list = font; - - return 1; - - fail: - if (font) - grub_free (font); - - if (file) - grub_file_close (file); - - return 0; -} - -static void -remove_font (struct font *font) -{ - struct font **p, *q; - - for (p = &font_list, q = *p; q; p = &(q->next), q = q->next) - if (q == font) - { - *p = q->next; - - grub_file_close (font->file); - grub_free (font); - - break; - } -} - -/* Return the offset of the glyph corresponding to the codepoint CODE - in the font FONT. If no found, return zero. */ -static grub_uint32_t -find_glyph (const struct font *font, grub_uint32_t code) -{ - grub_uint32_t start = 0; - grub_uint32_t end = font->num - 1; - const struct entry *table = font->table; - - /* This shouldn't happen. */ - if (font->num == 0) - return 0; - - /* Do a binary search. */ - while (start <= end) - { - grub_uint32_t i = (start + end) / 2; - - if (table[i].code < code) - start = i + 1; - else if (table[i].code > code) - end = i - 1; - else - return table[i].offset; - } - - return 0; -} - -/* Set the glyph to something stupid. */ -static void -fill_with_default_glyph (grub_font_glyph_t glyph) -{ - unsigned i; - - /* Use pre-defined pattern to fill unknown glyphs. */ - for (i = 0; i < 16; i++) - glyph->bitmap[i] = unknown_glyph[i]; - - glyph->char_width = 1; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; -} - -/* Get a glyph corresponding to the codepoint CODE. Always fill glyph - information with something, even if no glyph is found. */ -int -grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph) -{ - struct font *font; - grub_uint8_t bitmap[32]; - - /* FIXME: It is necessary to cache glyphs! */ - - restart: - for (font = font_list; font; font = font->next) - { - grub_uint32_t offset; - - offset = find_glyph (font, code); - if (offset) - { - grub_uint32_t w; - int len; - - /* Make sure we can find glyphs for error messages. Push active - error message to error stack and reset error message. */ - grub_error_push (); - - grub_file_seek (font->file, offset); - if ((len = grub_file_read (font->file, (char *) &w, sizeof (w))) - != sizeof (w)) - { - remove_font (font); - goto restart; - } - - w = grub_le_to_cpu32 (w); - if (w != 1 && w != 2) - { - /* grub_error (GRUB_ERR_BAD_FONT, "invalid width"); */ - remove_font (font); - goto restart; - } - - if (grub_file_read (font->file, (char *) bitmap, w * 16) - != (grub_ssize_t) w * 16) - { - remove_font (font); - goto restart; - } - - /* Fill glyph with information. */ - grub_memcpy (glyph->bitmap, bitmap, w * 16); - - glyph->char_width = w; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; - - /* Restore old error message. */ - grub_error_pop (); - - return 1; - } - } - - /* Uggh... No font was found. */ - fill_with_default_glyph (glyph); - return 0; -} - -static grub_err_t -font_command (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ - if (argc == 0) - return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); - - while (argc--) - if (! add_font (*args++)) - return 1; - - return 0; -} - -GRUB_MOD_INIT(font_manager) -{ - grub_register_command ("font", font_command, GRUB_COMMAND_FLAG_BOTH, - "font FILE...", - "Specify one or more font files to display.", 0); -} - -GRUB_MOD_FINI(font_manager) -{ - grub_unregister_command ("font"); -} === modified file 'genmk.rb' --- genmk.rb 2008-07-02 18:03:23 +0000 +++ genmk.rb 2008-07-04 21:40:36 +0000 @@ -125,7 +125,7 @@ $(TARGET_CC) $(TARGET_CPPFLAGS) $(TARGET_CFLAGS) $(#{prefix}_CFLAGS) -c -o $@ $< #{mod_src}: moddep.lst genmodsrc.sh - sh $(srcdir)/genmodsrc.sh '#{mod_name}' $< > $@ || (rm -f $@; exit 1) + $(GENMODSRC) '#{mod_name}' $< > $@ || (rm -f $@; exit 1) ifneq ($(#{prefix}_EXPORTS),no) #{defsym}: #{pre_obj} @@ -157,18 +157,21 @@ PARTMAPFILES += #{partmap} #{command}: #{src} $(#{src}_DEPENDENCIES) gencmdlist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENCMDLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/gencmdlist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{fs}: #{src} $(#{src}_DEPENDENCIES) genfslist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENFSLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genfslist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{partmap}: #{src} $(#{src}_DEPENDENCIES) genpartmaplist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENPARTMAPLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genpartmaplist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) === added file 'include/grub/bitmap_scale.h' --- include/grub/bitmap_scale.h 1970-01-01 00:00:00 +0000 +++ include/grub/bitmap_scale.h 2008-07-03 14:27:43 +0000 @@ -0,0 +1,54 @@ +/* bitmap_scale.h - Bitmap scaling functions. */ +/* + * 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 . + */ + +#ifndef GRUB_BITMAP_SCALE_HEADER +#define GRUB_BITMAP_SCALE_HEADER 1 + +#include +#include +#include + +enum grub_video_bitmap_scale_method +{ + /* Choose the fastest interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST, + /* Choose the highest quality interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST, + /* Nearest neighbor interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST, + /* Bilinear interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR +}; + +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum + grub_video_bitmap_scale_method scale_method); + +#endif /* ! GRUB_BITMAP_SCALE_HEADER */ === modified file 'include/grub/font.h' --- include/grub/font.h 2007-07-21 22:32:33 +0000 +++ include/grub/font.h 2008-07-03 14:17:31 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2007 Free Software Foundation, Inc. + * Copyright (C) 2003,2007,2008 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 @@ -21,33 +21,85 @@ #include -#define GRUB_FONT_MAGIC "PPF\x7f" +/* Forward declaration of opaque structure grub_font. + * Users only pass struct grub_font pointers to the font module functions, + * and do not have knowledge of the structure contents. */ +struct grub_font; + +/* Font type used to access font functions. */ +typedef struct grub_font *grub_font_t; + struct grub_font_glyph { - /* Glyph width in pixels. */ - grub_uint8_t width; - - /* Glyph height in pixels. */ - grub_uint8_t height; - - /* Glyph width in characters. */ - grub_uint8_t char_width; - - /* Glyph baseline position in pixels (from up). */ - grub_uint8_t baseline; - - /* Glyph bitmap data array of bytes in ((width + 7) / 8) * height. - Bitmap is formulated by height scanlines, each scanline having - width number of pixels. Pixels are coded as bits, value 1 meaning - of opaque pixel and 0 is transparent. If width does not fit byte - boundary, it will be padded with 0 to make it fit. */ - grub_uint8_t bitmap[32]; + /* Reference to the font this glyph belongs to. */ + grub_font_t font; + + /* Glyph bitmap width in pixels. */ + grub_uint16_t width; + + /* Glyph bitmap height in pixels. */ + grub_uint16_t height; + + /* Glyph bitmap x offset in pixels. Add to screen coordinate. */ + grub_int16_t offset_x; + + /* Glyph bitmap y offset in pixels. Subtract from screen coordinate. */ + grub_int16_t offset_y; + + /* Number of pixels to advance to start the next character. */ + grub_uint16_t device_width; + + /* Row-major order, packed bits (no padding; rows can break within a byte). + * The length of the array is (width * height + 7) / 8. Within a + * byte, the most significant bit is the first (leftmost/uppermost) pixel. + * Pixels are coded as bits, value 1 meaning of opaque pixel and 0 is + * transparent. If the length of the array does not fit byte boundary, it + * will be padded with 0 bits to make it fit. */ + grub_uint8_t bitmap[0]; }; -typedef struct grub_font_glyph *grub_font_glyph_t; - -int grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph); + +/****** font/font.c ******/ + +/* Get the font that has the specified name. Font names are in the form + * "Family Name Bold Italic 14", where Bold and Italic are optional. + * If no font matches the name specified, the most recently loaded font + * is returned as a fallback. */ +grub_font_t grub_font_get (const char *font_name); + +const char *grub_font_get_name (grub_font_t font); + +int grub_font_get_max_char_width (grub_font_t font); + +int grub_font_get_max_char_height (grub_font_t font); + +int grub_font_get_ascent (grub_font_t font); + +int grub_font_get_descent (grub_font_t font); + +int grub_font_get_string_width (grub_font_t font, const char *str); + + +/****** font/loader.c ******/ + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int grub_font_load (const char *filename); + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph *grub_font_get_glyph (grub_font_t font, + grub_uint32_t code); + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph *grub_font_get_glyph_any (grub_uint32_t code); #endif /* ! GRUB_FONT_HEADER */ === added file 'include/grub/font_internal.h' --- include/grub/font_internal.h 1970-01-01 00:00:00 +0000 +++ include/grub/font_internal.h 2008-07-03 14:16:11 +0000 @@ -0,0 +1,71 @@ +/* font_internal.h - Font declarations for use internally by the font module. + * Users of the font module should not include this header. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 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 . + */ + +#ifndef GRUB_FONT_INTERNAL_HEADER +#define GRUB_FONT_INTERNAL_HEADER 1 + +#include +#include +#include +#include +#include +#include +#include + +#define FONT_DEBUG 0 + +struct char_index_entry +{ + grub_uint32_t code; + grub_uint8_t storage_flags; + grub_uint32_t offset; + struct grub_font_glyph *glyph; /* Glyph if loaded, or null. */ +}; + +struct grub_font +{ + char *name; + grub_file_t file; + short max_char_width; + short max_char_height; + short ascent; + short descent; + grub_uint32_t num_chars; + struct char_index_entry *char_index; +}; + +struct font_node +{ + struct font_node *next; + struct grub_font *value; +}; + +extern struct font_node *grub_font_list; + + +/****** loader.c ******/ + +/* Initialize the font loader module. */ +void +grub_font_loader_init (void); + + +#endif /* ! GRUB_FONT_INTERNAL_HEADER */ + === modified file 'include/grub/i386/pc/vbeblit.h' --- include/grub/i386/pc/vbeblit.h 2007-07-21 22:32:33 +0000 +++ include/grub/i386/pc/vbeblit.h 2008-07-03 13:49:18 +0000 @@ -25,55 +25,75 @@ struct grub_video_i386_vbeblit_info; void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8X8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_index_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_index_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_index_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_BGRX8888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_BGRA8888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_BGR888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGBA8888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +/* Direct copy for compatible 32 bpp blit formats. + * (RGBA8888->RGBA8888, BGRA8888->BGRA8888, etc.) */ +void +grub_video_i386_vbeblit_direct32_copy (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGB888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGB888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_index_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_index_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGBA8888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGB888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_index_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y); === modified file 'include/grub/i386/pc/vbefill.h' --- include/grub/i386/pc/vbefill.h 2007-07-21 22:32:33 +0000 +++ include/grub/i386/pc/vbefill.h 2008-07-03 13:49:18 +0000 @@ -25,14 +25,14 @@ struct grub_video_i386_vbeblit_info; void -grub_video_i386_vbefill_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbefill_direct32 (struct grub_video_i386_vbeblit_info *dst, grub_video_color_t color, int x, int y, int width, int height); void -grub_video_i386_vbefill_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - grub_video_color_t color, int x, int y, - int width, int height); +grub_video_i386_vbefill_direct24 (struct grub_video_i386_vbeblit_info *dst, + grub_video_color_t color, int x, int y, + int width, int height); void grub_video_i386_vbefill_index (struct grub_video_i386_vbeblit_info *dst, === added file 'include/grub/i386/tsc.h' --- include/grub/i386/tsc.h 1970-01-01 00:00:00 +0000 +++ include/grub/i386/tsc.h 2008-07-04 17:55:21 +0000 @@ -0,0 +1,43 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 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 . + */ + +#ifndef KERNEL_CPU_TSC_HEADER +#define KERNEL_CPU_TSC_HEADER 1 + +#include + +/* Read the TSC value, which increments with each CPU clock cycle. */ +static __inline grub_uint64_t +grub_get_tsc (void) +{ + grub_uint32_t lo, hi; + + /* The CPUID instruction is a 'serializing' instruction, and + avoids out-of-order execution of the RDTSC instruction. */ + __asm__ __volatile__ ("xorl %%eax, %%eax\n\t" + "cpuid":::"%rax", "%rbx", "%rcx", "%rdx"); + /* Read TSC value. We cannot use "=A", since this would use + %rax on x86_64. */ + __asm__ __volatile__ ("rdtsc":"=a" (lo), "=d" (hi)); + + return (((grub_uint64_t) hi) << 32) | lo; +} + +void grub_tsc_calibrate (void); + +#endif /* ! KERNEL_CPU_TSC_HEADER */ === modified file 'include/grub/time.h' --- include/grub/time.h 2007-10-22 19:02:16 +0000 +++ include/grub/time.h 2008-07-03 04:19:16 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2007 Free Software Foundation, Inc. + * Copyright (C) 2007, 2008 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 @@ -19,12 +19,13 @@ #ifndef KERNEL_TIME_HEADER #define KERNEL_TIME_HEADER 1 +#include #include #include #include void EXPORT_FUNC(grub_millisleep) (grub_uint32_t ms); -void EXPORT_FUNC(grub_millisleep_generic) (grub_uint32_t ms); +grub_uint64_t EXPORT_FUNC(grub_get_time_ms) (void); static __inline void grub_sleep (grub_uint32_t s) === modified file 'include/grub/video.h' --- include/grub/video.h 2008-01-01 12:02:07 +0000 +++ include/grub/video.h 2008-07-03 14:12:08 +0000 @@ -21,6 +21,7 @@ #include #include +#include /* Video color in hardware dependent format. Users should not assume any specific coding format. */ @@ -31,40 +32,41 @@ struct grub_video_render_target; /* Forward declarations for used data structures. */ -struct grub_font_glyph; struct grub_video_bitmap; /* Defines used to describe video mode or rendering target. */ -#define GRUB_VIDEO_MODE_TYPE_ALPHA 0x00000008 -#define GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED 0x00000004 +#define GRUB_VIDEO_MODE_TYPE_ALPHA 0x00000020 +#define GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED 0x00000010 +#define GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP 0x00000004 #define GRUB_VIDEO_MODE_TYPE_INDEX_COLOR 0x00000002 #define GRUB_VIDEO_MODE_TYPE_RGB 0x00000001 /* Defines used to mask flags. */ -#define GRUB_VIDEO_MODE_TYPE_COLOR_MASK 0x00000003 +#define GRUB_VIDEO_MODE_TYPE_COLOR_MASK 0x0000000F /* Defines used to specify requested bit depth. */ #define GRUB_VIDEO_MODE_TYPE_DEPTH_MASK 0x0000ff00 #define GRUB_VIDEO_MODE_TYPE_DEPTH_POS 8 -/* Defined predefined render targets. */ -#define GRUB_VIDEO_RENDER_TARGET_DISPLAY ((struct grub_video_render_target *) 0) -#define GRUB_VIDEO_RENDER_TARGET_FRONT_BUFFER ((struct grub_video_render_target *) 0) -#define GRUB_VIDEO_RENDER_TARGET_BACK_BUFFER ((struct grub_video_render_target *) 1) +/* Predefined render target: */ +/* The render target that client code should render to. */ +#define GRUB_VIDEO_RENDER_TARGET_DISPLAY \ + ((struct grub_video_render_target *) 0) + /* Defined blitting formats. */ enum grub_video_blit_format { - /* Follow exactly field & mask information. */ - GRUB_VIDEO_BLIT_FORMAT_RGBA, - /* Make optimization assumption. */ - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8, - /* Follow exactly field & mask information. */ - GRUB_VIDEO_BLIT_FORMAT_RGB, - /* Make optimization assumption. */ - GRUB_VIDEO_BLIT_FORMAT_R8G8B8, + GRUB_VIDEO_BLIT_FORMAT_RGBA, /* General: Use fields & masks. */ + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_BGRA_8888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_RGB, /* General: Use fields & masks. */ + GRUB_VIDEO_BLIT_FORMAT_RGB_888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_BGR_888, /* Optimized format. */ /* When needed, decode color or just use value as is. */ - GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR + GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR, + /* Two color bitmap; bits packed: rows are not padded to byte boundary. */ + GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED }; /* Define blitting operators. */ @@ -127,6 +129,18 @@ /* What is location of reserved color bits. In Index Color mode, this is 0. */ unsigned int reserved_field_pos; + + /* For 1-bit bitmaps, the background color. Used for bits = 0. */ + grub_uint8_t bg_red; + grub_uint8_t bg_green; + grub_uint8_t bg_blue; + grub_uint8_t bg_alpha; + + /* For 1-bit bitmaps, the foreground color. Used for bits = 1. */ + grub_uint8_t fg_red; + grub_uint8_t fg_green; + grub_uint8_t fg_blue; + grub_uint8_t fg_alpha; }; struct grub_video_palette_data @@ -183,6 +197,10 @@ grub_err_t (*blit_glyph) (struct grub_font_glyph *glyph, grub_video_color_t color, int x, int y); + grub_err_t (*draw_string) (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y); + grub_err_t (*blit_bitmap) (struct grub_video_bitmap *bitmap, enum grub_video_blit_operators oper, int x, int y, int offset_x, int offset_y, @@ -255,6 +273,10 @@ grub_err_t grub_video_blit_glyph (struct grub_font_glyph *glyph, grub_video_color_t color, int x, int y); +grub_err_t grub_video_draw_string (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y); + grub_err_t grub_video_blit_bitmap (struct grub_video_bitmap *bitmap, enum grub_video_blit_operators oper, int x, int y, int offset_x, int offset_y, @@ -269,6 +291,9 @@ grub_err_t grub_video_scroll (grub_video_color_t color, int dx, int dy); +/* Swap the pages referred to by the front buffer and back buffer render + * targets, and the page previously referred to by the back buffer is made + * visible on the display. */ grub_err_t grub_video_swap_buffers (void); grub_err_t grub_video_create_render_target (struct grub_video_render_target **result, === added directory 'kern/generic' === added file 'kern/generic/get_time_ms.c' --- kern/generic/get_time_ms.c 1970-01-01 00:00:00 +0000 +++ kern/generic/get_time_ms.c 2008-07-04 16:55:48 +0000 @@ -0,0 +1,37 @@ +/* get_time_ms.c - generic time implementation -- using platform RTC. + * The generic implementation of these functions can be used for architectures + * or platforms that do not have a more specialized implementation. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 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 . + */ + +#include +#include + +/* Calculate the time in milliseconds since the epoch based on the RTC. */ +grub_uint64_t +grub_get_time_ms (void) +{ + /* By dimensional analysis: + + 1000 ms N rtc ticks 1 s + ------- * ----------- * ----------- = 1000*N/T ms + 1 s 1 T rtc ticks + */ + grub_uint64_t ticks_ms_per_sec = ((grub_uint64_t) 1000) * grub_get_rtc (); + return grub_divmod64 (ticks_ms_per_sec, GRUB_TICKS_PER_SECOND, 0); +} === added file 'kern/generic/millisleep.c' --- kern/generic/millisleep.c 1970-01-01 00:00:00 +0000 +++ kern/generic/millisleep.c 2008-07-04 16:55:48 +0000 @@ -0,0 +1,39 @@ +/* millisleep.c - generic millisleep function. + * The generic implementation of these functions can be used for architectures + * or platforms that do not have a more specialized implementation. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 1999,2000,2001,2002,2003,2004,2005,2006,2007,2008 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 . + */ + +#include +#include + +void +grub_millisleep (grub_uint32_t ms) +{ + grub_uint64_t start; + + start = grub_get_time_ms (); + + /* Instead of setting an end time and looping while the current time is + less than that, comparing the elapsed sleep time with the desired sleep + time handles the (unlikely!) case that the timer would wrap around + during the sleep. */ + + while (grub_get_time_ms () - start < ms) + grub_cpu_idle (); +} === modified file 'kern/i386/efi/init.c' --- kern/i386/efi/init.c 2007-10-22 18:59:33 +0000 +++ kern/i386/efi/init.c 2008-07-04 17:55:21 +0000 @@ -25,18 +25,13 @@ #include #include #include -#include - -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} +#include void grub_machine_init (void) { grub_efi_init (); + grub_tsc_calibrate (); } void === modified file 'kern/i386/pc/init.c' --- kern/i386/pc/init.c 2008-06-15 17:21:16 +0000 +++ kern/i386/pc/init.c 2008-07-04 18:03:26 +0000 @@ -30,6 +30,7 @@ #include #include #include +#include struct mem_region { @@ -46,12 +47,6 @@ grub_size_t grub_os_area_size; grub_size_t grub_lower_mem, grub_upper_mem; -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - void grub_arch_sync_caches (void *address __attribute__ ((unused)), grub_size_t len __attribute__ ((unused))) @@ -231,6 +226,8 @@ if (! grub_os_area_addr) grub_fatal ("no upper memory"); + + grub_tsc_calibrate (); } void === added file 'kern/i386/tsc.c' --- kern/i386/tsc.c 1970-01-01 00:00:00 +0000 +++ kern/i386/tsc.c 2008-07-04 17:55:21 +0000 @@ -0,0 +1,89 @@ +/* kern/i386/tsc.c - x86 TSC time source implementation + * Requires Pentium or better x86 CPU that supports the RDTSC instruction. + * This module uses the RTC (via grub_get_rtc()) to calibrate the TSC to + * real time. + * + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 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 . + */ + +#include +#include +#include +#include + +/* Calibrated reference for TSC=0. This defines the time since the epoch in + milliseconds that TSC=0 refers to. */ +static grub_uint64_t tsc_boot_time; + +/* Calibrated TSC rate. (In TSC ticks per millisecond.) */ +static grub_uint64_t tsc_ticks_per_ms; + + +/* Declared in . */ +grub_uint64_t +grub_get_time_ms (void) +{ + return tsc_boot_time + grub_divmod64 (grub_get_tsc (), tsc_ticks_per_ms, 0); +} + + +/* How many RTC ticks to use for calibration loop. (>= 1) */ +#define CALIBRATION_TICKS 2 + +/* Calibrate the TSC based on the RTC. */ +void +grub_tsc_calibrate (void) +{ + /* First calbrate the TSC rate (relative, not absolute time). */ + grub_uint64_t start_tsc; + grub_uint64_t end_tsc; + grub_uint32_t initial_tick; + grub_uint32_t start_tick; + grub_uint32_t end_tick; + + /* Wait for the start of the next tick; + we'll base out timing off this edge. */ + initial_tick = grub_get_rtc (); + do + { + start_tick = grub_get_rtc (); + } + while (start_tick == initial_tick); + start_tsc = grub_get_tsc (); + + /* Wait for the start of the next tick. This will + be the end of the 1-tick period. */ + do + { + end_tick = grub_get_rtc (); + } + while (end_tick - start_tick < CALIBRATION_TICKS); + end_tsc = grub_get_tsc (); + + tsc_ticks_per_ms = + grub_divmod64 (grub_divmod64 + (end_tsc - start_tsc, end_tick - start_tick, 0) + * GRUB_TICKS_PER_SECOND, 1000, 0); + + /* Reference the TSC zero (boot time) to the epoch to + get an absolute real time reference. */ + grub_uint64_t ms_since_boot = grub_divmod64 (end_tsc, tsc_ticks_per_ms, 0); + grub_uint64_t mstime_now = grub_divmod64 ((grub_uint64_t) 1000 * end_tick, + GRUB_TICKS_PER_SECOND, + 0); + tsc_boot_time = mstime_now - ms_since_boot; +} === modified file 'kern/ieee1275/init.c' --- kern/ieee1275/init.c 2008-07-04 02:01:55 +0000 +++ kern/ieee1275/init.c 2008-07-04 19:43:25 +0000 @@ -47,12 +47,6 @@ extern char _end[]; void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - -void grub_exit (void) { grub_ieee1275_exit (); @@ -260,8 +254,8 @@ grub_console_fini (); } -grub_uint32_t -grub_get_rtc (void) +grub_uint64_t +grub_get_time_ms (void) { grub_uint32_t msecs = 0; @@ -270,6 +264,12 @@ return msecs; } +grub_uint32_t +grub_get_rtc (void) +{ + return grub_get_time_ms (); +} + grub_addr_t grub_arch_modules_addr (void) { === modified file 'kern/misc.c' --- kern/misc.c 2008-06-15 23:42:48 +0000 +++ kern/misc.c 2008-07-04 18:03:26 +0000 @@ -23,7 +23,6 @@ #include #include #include -#include void * grub_memmove (void *dest, const void *src, grub_size_t n) @@ -1018,17 +1017,6 @@ return p - dest; } -void -grub_millisleep_generic (grub_uint32_t ms) -{ - grub_uint32_t end_at; - - end_at = grub_get_rtc () + grub_div_roundup (ms * GRUB_TICKS_PER_SECOND, 1000); - - while (grub_get_rtc () < end_at) - grub_cpu_idle (); -} - /* Abort GRUB. This function does not return. */ void grub_abort (void) === modified file 'kern/sparc64/ieee1275/init.c' --- kern/sparc64/ieee1275/init.c 2007-10-22 18:59:33 +0000 +++ kern/sparc64/ieee1275/init.c 2008-07-04 17:55:21 +0000 @@ -66,12 +66,6 @@ /* Never reached. */ } -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - int grub_ieee1275_test_flag (enum grub_ieee1275_flag flag) { === added directory 'protomenu' === added file 'protomenu/protomenu.c' --- protomenu/protomenu.c 1970-01-01 00:00:00 +0000 +++ protomenu/protomenu.c 2008-07-05 17:59:13 +0000 @@ -0,0 +1,277 @@ +/* protomenu.c - Prototype graphical menu: command implementation. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2007 Free Software Foundation, Inc. + * Copyright (C) 2003 NIIBE Yutaka + * + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* A 2D rectangle type. + * This could be worth integrating into the video API if it proves useful.*/ +struct grub_video_rect +{ + /* These are signed because if there are unsigned it causes Bad Things + * to happen when arithmetic and comparisions involving signed types is + * done. Important signed types include offsets from absolute locations. */ + int x; + int y; + int width; + int height; +}; +typedef struct grub_video_rect grub_video_rect_t; + +typedef struct menu_item +{ + const char *name; + struct grub_video_bitmap *icon; + /* TODO add a command to execute. */ +} +menu_item_t; + +static const int icon_width = 48; +static const int icon_height = 48; +static grub_video_rect_t screen; +static grub_font_t title_font; +static grub_font_t item_font; +static grub_font_t status_font; +static grub_video_color_t title_color; +static grub_video_color_t item_color; +static grub_video_color_t status_color; +static grub_video_color_t status_bg_color; +static struct grub_video_bitmap *background_image; +static const char title_text[] = "GRUB 2 Boot Menu"; +static menu_item_t items[3]; +static int selected_item_index; + +static void +style_init (void) +{ + if (!(title_font = grub_font_get ("Helvetica Bold 24")) + || !(item_font = grub_font_get ("Helvetica Bold 14")) + || !(status_font = grub_font_get ("Helvetica 10"))) + return; + + title_color = grub_video_map_rgb (0, 0, 0); + item_color = grub_video_map_rgb (0, 0, 0); + status_color = grub_video_map_rgb (255, 255, 255); + status_bg_color = grub_video_map_rgba (0, 0, 0, 112); + + struct grub_video_bitmap *img; + grub_video_bitmap_load (&img, "/boot/images/bg-protomenu1.tga"); + if (grub_errno != GRUB_ERR_NONE) + return; + + grub_video_bitmap_create_scaled (&background_image, screen.width, + screen.height, img, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno != GRUB_ERR_NONE) + return; + grub_video_bitmap_destroy (img); + + + items[0].name = "Ubuntu 8.10"; + grub_video_bitmap_load (&img, "/boot/images/icon-ubuntu.tga"); + if (grub_errno == GRUB_ERR_NONE) + { + grub_video_bitmap_create_scaled (&items[0].icon, icon_width, + icon_height, img, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno != GRUB_ERR_NONE) + return; + grub_video_bitmap_destroy (img); + } + + items[1].name = "Gentoo Linux"; + grub_video_bitmap_load (&img, "/boot/images/icon-gentoo.tga"); + if (grub_errno == GRUB_ERR_NONE) + { + grub_video_bitmap_create_scaled (&items[1].icon, icon_width, + icon_height, img, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno != GRUB_ERR_NONE) + return; + grub_video_bitmap_destroy (img); + } + + items[2].name = "Windows Vista"; + grub_video_bitmap_load (&img, "/boot/images/icon-vista.tga"); + if (grub_errno == GRUB_ERR_NONE) + { + grub_video_bitmap_create_scaled (&items[2].icon, icon_width, + icon_height, img, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno != GRUB_ERR_NONE) + return; + grub_video_bitmap_destroy (img); + } +} + +static void +draw_background (void) +{ + grub_video_blit_bitmap (background_image, GRUB_VIDEO_BLIT_REPLACE, + 0, 0, 0, 0, + grub_video_bitmap_get_width (background_image), + grub_video_bitmap_get_height (background_image)); +} + +static void +draw_menu (void) +{ + /* TODO make this use pixmaps and draw a cool menu */ + + grub_video_rect_t r; + r.width = screen.width * 2 / 3; + r.height = screen.height * 1 / 2; + r.x = (screen.width - r.width) / 2; + r.y = (screen.height - r.height) / 2; + + grub_video_color_t bg = grub_video_map_rgb (255, 255, 255); + grub_video_fill_rect (bg, r.x, r.y, r.width, r.height); + + int boxpad = 6; + int icon_text_space = 4; + int item_pad = 3; + int item_vspace = 2; + + int ascent = grub_font_get_ascent (item_font); + int descent = grub_font_get_descent (item_font); + int item_top = r.y + boxpad; + int item_left = r.x + boxpad; + int item_height = 54; + unsigned i; + for (i = 0; i < sizeof (items) / sizeof (*items); i++) + { + if ((int) i == selected_item_index) + { + /* TODO use pixmap-stylized box to highlight selected item */ + grub_video_fill_rect (grub_video_map_rgb (128, 128, 255), + item_left, item_top, + r.width - 2 * boxpad, item_height); + } + + if (items[i].icon) + grub_video_blit_bitmap (items[i].icon, GRUB_VIDEO_BLIT_BLEND, + item_left, + item_top + (item_height - icon_height) / 2, + 0, 0, icon_width, icon_height); + + grub_video_draw_string (items[i].name, item_font, item_color, + item_left + icon_width + icon_text_space, + (item_top + (item_height - (ascent + descent)) + / 2 + ascent)); + + item_top += item_height + item_vspace; + } +} + +static void +draw_title (void) +{ + /* Center the title. */ + int title_width = grub_font_get_string_width (title_font, title_text); + int x = (screen.width - title_width) / 2; + int y = 40 + grub_font_get_ascent (title_font); + grub_video_draw_string (title_text, title_font, title_color, x, y); +} + +static void +draw_status (void) +{ + int descent = grub_font_get_descent (status_font); + int ascent = grub_font_get_ascent (status_font); + int vpad = 5; + int textheight = descent + ascent; + int h = 2 * vpad + textheight; + + grub_video_fill_rect (status_bg_color, 0, screen.height - h, + screen.width, screen.height - 1); + + int texty = screen.height - vpad - descent; + grub_video_draw_string ("Select an item with the arrow keys and " + "press Enter to boot.", + status_font, status_color, 30, texty); +} + +static grub_err_t +grub_cmd_protomenu (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + if (grub_video_setup (1024, 768, GRUB_VIDEO_MODE_TYPE_RGB) != GRUB_ERR_NONE) + return grub_errno; + + grub_video_get_viewport ((unsigned *) &screen.x, + (unsigned *) &screen.y, + (unsigned *) &screen.width, + (unsigned *) &screen.height); + + style_init (); + selected_item_index = 0; + + int done = 0; + while (!done) + { + draw_background (); + draw_menu (); + draw_title (); + draw_status (); + + int c = GRUB_TERM_ASCII_CHAR (grub_getkey ()); + if (c == 'j') + { + selected_item_index++; + if (selected_item_index >= (int) (sizeof (items) / sizeof (*items))) + selected_item_index = 0; + } + else if (c == 'k') + { + selected_item_index--; + if (selected_item_index < 0) + selected_item_index = (sizeof (items) / sizeof (*items)) - 1; + } + else if (c == GRUB_TERM_ESC) + { + done = 1; + } + } + + grub_video_restore (); + return 0; +} + +GRUB_MOD_INIT (protomenu) +{ + (void) mod; /* To stop warning. */ + grub_register_command ("protomenu", + grub_cmd_protomenu, GRUB_COMMAND_FLAG_BOTH, + "protomenu", "Show graphical menu prototype", 0); +} + +GRUB_MOD_FINI (protomenu) +{ + grub_unregister_command ("protomenu"); +} === modified file 'term/gfxterm.c' --- term/gfxterm.c 2008-01-01 12:02:07 +0000 +++ term/gfxterm.c 2008-07-03 14:28:01 +0000 @@ -29,14 +29,12 @@ #include #include #include +#include #define DEFAULT_VIDEO_WIDTH 640 #define DEFAULT_VIDEO_HEIGHT 480 #define DEFAULT_VIDEO_FLAGS 0 -#define DEFAULT_CHAR_WIDTH 8 -#define DEFAULT_CHAR_HEIGHT 16 - #define DEFAULT_BORDER_WIDTH 10 #define DEFAULT_STANDARD_COLOR 0x07 @@ -91,6 +89,9 @@ unsigned int cursor_y; int cursor_state; + /* Font settings. */ + grub_font_t font; + /* Terminal color settings. */ grub_uint8_t standard_color_setting; grub_uint8_t normal_color_setting; @@ -170,18 +171,25 @@ static grub_err_t grub_virtual_screen_setup (unsigned int x, unsigned int y, - unsigned int width, unsigned int height) + unsigned int width, unsigned int height, + const char *font_name) { /* Free old virtual screen. */ grub_virtual_screen_free (); /* Initialize with default data. */ + virtual_screen.font = grub_font_get (font_name); + if (!virtual_screen.font) + return grub_error (GRUB_ERR_BAD_FONT, + "No font loaded."); virtual_screen.width = width; virtual_screen.height = height; virtual_screen.offset_x = x; virtual_screen.offset_y = y; - virtual_screen.char_width = DEFAULT_CHAR_WIDTH; - virtual_screen.char_height = DEFAULT_CHAR_HEIGHT; + virtual_screen.char_width = + grub_font_get_max_char_width (virtual_screen.font); + virtual_screen.char_height = + grub_font_get_max_char_height (virtual_screen.font); virtual_screen.cursor_x = 0; virtual_screen.cursor_y = 0; virtual_screen.cursor_state = 1; @@ -229,6 +237,7 @@ static grub_err_t grub_gfxterm_init (void) { + char *font_name; char *modevar; int width = DEFAULT_VIDEO_WIDTH; int height = DEFAULT_VIDEO_HEIGHT; @@ -236,6 +245,11 @@ int flags = DEFAULT_VIDEO_FLAGS; grub_video_color_t color; + /* Select the font to use. */ + font_name = grub_env_get ("gfxterm_font"); + if (!font_name) + font_name = ""; /* Allow fallback to any font. */ + /* Parse gfxmode environment variable if set. */ modevar = grub_env_get ("gfxmode"); if (modevar) @@ -475,7 +489,7 @@ /* Create virtual screen. */ if (grub_virtual_screen_setup (DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_WIDTH, - width, height) != GRUB_ERR_NONE) + width, height, font_name) != GRUB_ERR_NONE) { grub_video_restore (); return grub_errno; @@ -661,11 +675,12 @@ write_char (void) { struct grub_colored_char *p; - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; grub_video_color_t color; grub_video_color_t bgcolor; unsigned int x; unsigned int y; + int ascent; /* Find out active character. */ p = (virtual_screen.text_buffer @@ -675,7 +690,8 @@ p -= p->index; /* Get glyph for character. */ - grub_font_get_glyph (p->code, &glyph); + glyph = grub_font_get_glyph (virtual_screen.font, p->code); + ascent = grub_font_get_ascent (virtual_screen.font); color = p->fg_color; bgcolor = p->bg_color; @@ -685,13 +701,13 @@ /* Render glyph to text layer. */ grub_video_set_active_render_target (text_layer); - grub_video_fill_rect (bgcolor, x, y, glyph.width, glyph.height); - grub_video_blit_glyph (&glyph, color, x, y); + grub_video_fill_rect (bgcolor, x, y, glyph->width, glyph->height); + grub_video_blit_glyph (glyph, color, x, y + ascent); grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); /* Mark character to be drawn. */ dirty_region_add (virtual_screen.offset_x + x, virtual_screen.offset_y + y, - glyph.width, glyph.height); + glyph->width, glyph->height); } static void @@ -705,7 +721,8 @@ /* Determine cursor properties and position on text layer. */ x = virtual_screen.cursor_x * virtual_screen.char_width; - y = ((virtual_screen.cursor_y + 1) * virtual_screen.char_height) - 3; + y = (virtual_screen.cursor_y * virtual_screen.char_height + + grub_font_get_ascent (virtual_screen.font)); width = virtual_screen.char_width; height = 2; @@ -822,14 +839,18 @@ } else { - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; struct grub_colored_char *p; + unsigned char_width; /* Get properties of the character. */ - grub_font_get_glyph (c, &glyph); + glyph = grub_font_get_glyph (virtual_screen.font, c); + + /* TODO [CDB] Fix wide characters. Bi-width font? */ + char_width = 1; /* If we are about to exceed line length, wrap to next line. */ - if (virtual_screen.cursor_x + glyph.char_width > virtual_screen.columns) + if (virtual_screen.cursor_x + char_width > virtual_screen.columns) grub_putchar ('\n'); /* Find position on virtual screen, and fill information. */ @@ -839,18 +860,18 @@ p->code = c; p->fg_color = virtual_screen.fg_color; p->bg_color = virtual_screen.bg_color; - p->width = glyph.char_width - 1; + p->width = char_width - 1; p->index = 0; /* If we have large glyph, add fixup info. */ - if (glyph.char_width > 1) + if (char_width > 1) { unsigned i; - for (i = 1; i < glyph.char_width; i++) + for (i = 1; i < char_width; i++) { p[i].code = ' '; - p[i].width = glyph.char_width - 1; + p[i].width = char_width - 1; p[i].index = i; } } @@ -859,7 +880,7 @@ write_char (); /* Make sure we scroll screen when needed and wrap line correctly. */ - virtual_screen.cursor_x += glyph.char_width; + virtual_screen.cursor_x += char_width; if (virtual_screen.cursor_x >= virtual_screen.columns) { virtual_screen.cursor_x = 0; @@ -879,11 +900,15 @@ static grub_ssize_t grub_gfxterm_getcharwidth (grub_uint32_t c) { - struct grub_font_glyph glyph; - - grub_font_get_glyph (c, &glyph); - - return glyph.char_width; +#if 0 + struct grub_font_glyph *glyph; + + glyph = grub_font_get_glyph (c); + + return glyph->char_width; +#else + return 1; /* TODO [CDB] Fix wide characters. */ +#endif } static grub_uint16_t @@ -1014,8 +1039,18 @@ dirty_region_redraw (); } + +/* Option array indices. */ +#define BACKGROUND_CMD_ARGINDEX_MODE 0 + +static const struct grub_arg_option background_image_cmd_options[] = { + {"mode", 'm', 0, "Background image mode (`stretch', `normal').", 0, + ARG_TYPE_STRING}, + {0, 0, 0, 0, 0, 0} +}; + static grub_err_t -grub_gfxterm_background_image_cmd (struct grub_arg_list *state __attribute__ ((unused)), +grub_gfxterm_background_image_cmd (struct grub_arg_list *state, int argc, char **args) { @@ -1042,12 +1077,36 @@ if (grub_errno != GRUB_ERR_NONE) return grub_errno; + /* Determine if the bitmap should be scaled to fit the screen. */ + if (!state[BACKGROUND_CMD_ARGINDEX_MODE].set + || grub_strcmp (state[BACKGROUND_CMD_ARGINDEX_MODE].arg, + "stretch") == 0) + { + if (mode_info.width != grub_video_bitmap_get_width (bitmap) + || mode_info.height != grub_video_bitmap_get_height (bitmap)) + { + struct grub_video_bitmap *scaled_bitmap; + grub_video_bitmap_create_scaled (&scaled_bitmap, + mode_info.width, + mode_info.height, + bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno == GRUB_ERR_NONE) + { + /* Replace the original bitmap with the scaled one. */ + grub_video_bitmap_destroy (bitmap); + bitmap = scaled_bitmap; + } + } + } + + /* If bitmap was loaded correctly, display it. */ if (bitmap) { /* Determine bitmap dimensions. */ bitmap_width = grub_video_bitmap_get_width (bitmap); - bitmap_height = grub_video_bitmap_get_width (bitmap); + bitmap_height = grub_video_bitmap_get_height (bitmap); /* Mark whole screen as dirty. */ dirty_region_reset (); @@ -1092,7 +1151,7 @@ GRUB_COMMAND_FLAG_BOTH, "background_image", "Load background image for active terminal", - 0); + background_image_cmd_options); } GRUB_MOD_FINI(term_gfxterm) === modified file 'term/i386/pc/vesafb.c' --- term/i386/pc/vesafb.c 2007-12-30 08:52:06 +0000 +++ term/i386/pc/vesafb.c 2008-07-03 14:12:08 +0000 @@ -250,10 +250,11 @@ break; default: - return grub_font_get_glyph (code, bitmap, width); + return grub_font_get_glyph_any (code, bitmap, width); } } + /* TODO [CDB] This is wrong for the new font module. Should it be fixed? */ if (bitmap) grub_memcpy (bitmap, vga_font + code * virtual_screen.char_height, === modified file 'term/i386/pc/vga.c' --- term/i386/pc/vga.c 2008-01-21 15:48:27 +0000 +++ term/i386/pc/vga.c 2008-07-03 14:12:08 +0000 @@ -65,6 +65,7 @@ static struct colored_char text_buf[TEXT_WIDTH * TEXT_HEIGHT]; static unsigned char saved_map_mask; static int page = 0; +static grub_font_t font = 0; #define SEQUENCER_ADDR_PORT 0x3C4 #define SEQUENCER_DATA_PORT 0x3C5 @@ -161,6 +162,9 @@ saved_map_mask = get_map_mask (); set_map_mask (0x0f); set_start_address (PAGE_OFFSET (page)); + font = grub_font_get (""); /* Choose any font, for now. */ + if (!font) + return grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); return GRUB_ERR_NONE; } @@ -185,7 +189,7 @@ write_char (void) { struct colored_char *p = text_buf + xpos + ypos * TEXT_WIDTH; - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; unsigned char *mem_base; unsigned plane; @@ -194,7 +198,7 @@ p -= p->index; /* Get glyph for character. */ - grub_font_get_glyph (p->code, &glyph); + glyph = grub_font_get_glyph (font, p->code); for (plane = 0x01; plane <= 0x08; plane <<= 1) { @@ -210,17 +214,21 @@ { unsigned i; - for (i = 0; i < glyph.char_width && offset < 32; i++) + unsigned char_width = 1; /* TODO [CDB] Figure out wide characters. */ + /* TODO [CDB] Re-implement glyph drawing for vga module. */ +#if 0 + for (i = 0; i < char_width && offset < 32; i++) { unsigned char fg_mask, bg_mask; - fg_mask = (p->fg_color & plane) ? glyph.bitmap[offset] : 0; - bg_mask = (p->bg_color & plane) ? ~(glyph.bitmap[offset]) : 0; + fg_mask = (p->fg_color & plane) ? glyph->bitmap[offset] : 0; + bg_mask = (p->bg_color & plane) ? ~(glyph->bitmap[offset]) : 0; offset++; if (check_vga_mem (mem + i)) mem[i] = (fg_mask | bg_mask); } +#endif /* 0 */ } } @@ -320,36 +328,37 @@ } else { - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; struct colored_char *p; + unsigned char_width = 1; - grub_font_get_glyph(c, &glyph); + glyph = grub_font_get_glyph(font, c); - if (xpos + glyph.char_width > TEXT_WIDTH) + if (xpos + char_width > TEXT_WIDTH) grub_putchar ('\n'); p = text_buf + xpos + ypos * TEXT_WIDTH; p->code = c; p->fg_color = fg_color; p->bg_color = bg_color; - p->width = glyph.char_width - 1; + p->width = char_width - 1; p->index = 0; - if (glyph.char_width > 1) + if (char_width > 1) { unsigned i; - for (i = 1; i < glyph.char_width; i++) + for (i = 1; i < char_width; i++) { p[i].code = ' '; - p[i].width = glyph.char_width - 1; + p[i].width = char_width - 1; p[i].index = i; } } write_char (); - xpos += glyph.char_width; + xpos += char_width; if (xpos >= TEXT_WIDTH) { xpos = 0; @@ -381,11 +390,16 @@ static grub_ssize_t grub_vga_getcharwidth (grub_uint32_t c) { +#if 0 struct grub_font_glyph glyph; - grub_font_get_glyph (c, &glyph); + glyph = grub_font_get_glyph (c); return glyph.char_width; +#else + /* TODO [CDB] Glyph wide characters? vga? */ + return 1; +#endif } static grub_uint16_t === added directory 'util/fonttool' === added file 'util/fonttool/build.xml' --- util/fonttool/build.xml 1970-01-01 00:00:00 +0000 +++ util/fonttool/build.xml 2008-07-03 14:08:39 +0000 @@ -0,0 +1,23 @@ + + GRUB 2 font tool. + + + + + + + + + + + + + + + + + + + + === added file 'util/fonttool/fonttool.iml' --- util/fonttool/fonttool.iml 1970-01-01 00:00:00 +0000 +++ util/fonttool/fonttool.iml 2008-07-03 14:08:39 +0000 @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + === added file 'util/fonttool/fonttool.ipr' --- util/fonttool/fonttool.ipr 1970-01-01 00:00:00 +0000 +++ util/fonttool/fonttool.ipr 2008-07-03 14:08:39 +0000 @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + === added directory 'util/fonttool/src' === added directory 'util/fonttool/src/org' === added directory 'util/fonttool/src/org/gnu' === added directory 'util/fonttool/src/org/gnu/grub' === added directory 'util/fonttool/src/org/gnu/grub/fonttool' === added file 'util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java' --- util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,24 @@ +package org.gnu.grub.fonttool; + +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; + +public class BDFFont { + private Map glyphs = new HashMap(); + private int height = 0; + + void putGlyph(char ch, BufferedImage glyph) { + glyphs.put(ch, glyph); + if (glyph.getHeight() > height) + height = glyph.getHeight(); + } + + public BufferedImage getGlyph(char c) { + return glyphs.get(c); + } + + public int getHeight() { + return height; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java' --- util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,254 @@ +package org.gnu.grub.fonttool; + +import java.io.*; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BDFLoader { + private final BufferedReader in; + private final Font font; + private int maxCharWidth; + private int maxCharHeight; + + BDFLoader(BufferedReader in) { + this.in = in; + this.font = new Font(); + this.maxCharWidth = 0; + this.maxCharHeight = 0; + } + + public static boolean isBDFFile(String filename) { + DataInputStream in = null; + try { + in = new DataInputStream(new FileInputStream(filename)); + final String signature = "STARTFONT "; + byte[] b = new byte[signature.length()]; + in.readFully(b); + in.close(); + + String s = new String(b, "US-ASCII"); + return signature.equals(s); + } catch (IOException e) { + if (in != null) { + try { + in.close(); + } catch (IOException e1) { + // Ignore. + } + } + return false; + } + } + + public static Font loadFontResource(String resourceName) throws IOException { + InputStream in = BDFLoader.class.getClassLoader().getResourceAsStream(resourceName); + if (in == null) + throw new FileNotFoundException("Font resource " + resourceName + " not found"); + return loadFontFromStream(in); + } + + public static Font loadFontFile(String filename) throws IOException { + InputStream in = new FileInputStream(filename); + return loadFontFromStream(in); + } + + private static Font loadFontFromStream(InputStream inStream) throws IOException { + BufferedReader in; + try { + in = new BufferedReader(new InputStreamReader(inStream, "ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Encoding not supported: " + e.getMessage(), e); + } + return new BDFLoader(in).loadFont(); + } + + private Font loadFont() throws IOException { + loadFontInfo(); + while (loadChar()) { + /* Loop. */ + } + + font.setMaxCharWidth(maxCharWidth); + font.setMaxCharHeight(maxCharHeight); + return font; + } + + private void loadFontInfo() throws IOException { + final Pattern stringSettingPattern = Pattern.compile("^(\\w+)\\s+\"([^\"]+)\"$"); + String line; + // Load the global font information that appears before CHARS. + final int UNSET = Integer.MIN_VALUE; + font.setAscent(UNSET); + font.setDescent(UNSET); + font.setFamily("Unknown"); + font.setBold(false); + font.setItalic(false); + font.setPointSize(Font.UNKNOWN_POINT_SIZE); + do { + line = in.readLine(); + if (line == null) + throw new IOException("BDF format error: end of file while " + + "reading global font information"); + + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens()) { + String name = st.nextToken(); + if (name.equals("FONT_ASCENT")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + font.setAscent(Integer.parseInt(st.nextToken())); + } else if (name.equals("FONT_DESCENT")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + font.setDescent(Integer.parseInt(st.nextToken())); + } else if (name.equals("POINT_SIZE")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + // Divide by 10, since it is stored X10. + font.setPointSize(Integer.parseInt(st.nextToken()) / 10); + } else if (name.equals("FAMILY_NAME")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + font.setFamily(matcher.group(2)); + } else if (name.equals("WEIGHT_NAME")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + String weightName = matcher.group(2); + font.setBold("bold".equalsIgnoreCase(weightName)); + } else if (name.equals("SLANT")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + String slantType = matcher.group(2); + font.setItalic(!"R".equalsIgnoreCase(slantType)); + } else if (name.equals("CHARS")) { + // This is the end of the global font information and + // the beginning of the character definitions. + break; + } else { + // Skip other fields. + } + } + } while (true); + + if (font.getAscent() == UNSET) + throw new IOException("BDF format error: no FONT_ASCENT property"); + if (font.getDescent() == UNSET) + throw new IOException("BDF format error: no FONT_DESCENT property"); + } + + private boolean loadChar() throws IOException { + String line; + // Find start of character + do { + line = in.readLine(); + if (line == null) + return false; + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens() && st.nextToken().equals("STARTCHAR")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no character name after STARTCHAR"); + break; + } + } while (true); + + // Find properties + final int UNSET = Integer.MIN_VALUE; + int codePoint = UNSET; + int bbx = UNSET; + int bby = UNSET; + int bbox = UNSET; + int bboy = UNSET; + int dwidth = UNSET; + do { + line = in.readLine(); + if (line == null) + return false; + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens()) { + String field = st.nextToken(); + if (field.equals("ENCODING")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no encoding # after ENCODING"); + String codePointStr = st.nextToken(); + codePoint = Integer.parseInt(codePointStr); + } else if (field.equals("BBX")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no tokens after BBX"); + bbx = Integer.parseInt(st.nextToken()); + bby = Integer.parseInt(st.nextToken()); + bbox = Integer.parseInt(st.nextToken()); + bboy = Integer.parseInt(st.nextToken()); + } else if (field.equals("DWIDTH")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no tokens after DWIDTH"); + dwidth = Integer.parseInt(st.nextToken()); + int dwidthY = Integer.parseInt(st.nextToken()); + // The DWIDTH Y value should be zero for any normal font. + if (dwidthY != 0) { + throw new IOException("BDF format error: dwidth Y value" + + "is nonzero (" + dwidthY + ") " + + "for char " + codePoint + "."); + } + } else if (field.equals("BITMAP")) { + break; // now read the bitmap + } + } + } while (true); + + if (codePoint == UNSET) + throw new IOException("BDF format error: " + + "no code point set"); + if (bbx == UNSET || bby == UNSET) + throw new IOException("BDF format error: " + + "bbx/bby missing: " + bbx + ", " + bby + + " for char " + codePoint); + + if (bbox == UNSET || bboy == UNSET) + throw new IOException("BDF format error: " + + "bbox/bboy missing: " + bbox + ", " + bboy + + " for char " + codePoint); + + if (dwidth == UNSET) + throw new IOException("BDF format error: " + + "dwidth missing for char " + codePoint); + + final int glyphWidth = bbx; + final int glyphHeight = bby; + if (glyphWidth > maxCharWidth) + maxCharWidth = glyphWidth; + if (glyphHeight > maxCharHeight) + maxCharHeight = glyphHeight; + + // Read the bitmap + Glyph glyph = new Glyph(codePoint, glyphWidth, glyphHeight, bbox, bboy, dwidth); + for (int y = 0; y < glyphHeight; y++) { + line = in.readLine(); + if (line == null) + return false; + for (int b = 0; b < line.length(); b++) { + int v = Integer.parseInt(Character.toString(line.charAt(b)), 16); + for (int x = b * 4, i = 0; i < 4 && x < glyphWidth; x++, i++) { + boolean set = (v & 0x8) != 0; + v <<= 1; + glyph.setPixel(x, y, set); + } + } + } + + font.putGlyph(codePoint, glyph); + return true; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,135 @@ +package org.gnu.grub.fonttool; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.TreeMap; + +class CharDefs { + private boolean debug = "1".equals(System.getProperty("fonttool.debug")); + private TreeMap glyphs; + private ByteArrayOutputStream charDefsData; + private int maxCharWidth; + private int maxCharHeight; + private HashMap charIndex; + + public CharDefs(Font font) { + this.glyphs = font.getGlyphs(); + this.charIndex = null; + this.charDefsData = null; + + calculateMaxSizes(); + } + + private void calculateMaxSizes() { + maxCharWidth = 0; + maxCharHeight = 0; + for (Glyph glyph : glyphs.values()) { + final int w = glyph.getWidth(); + final int h = glyph.getHeight(); + if (w > maxCharWidth) + maxCharWidth = w; + if (h > maxCharHeight) + maxCharHeight = h; + } + } + + void buildDefinitions() { + charIndex = new HashMap(); + HashMap charDefIndex = new HashMap(); + charDefsData = new ByteArrayOutputStream(); + DataOutputStream charDefs = new DataOutputStream(charDefsData); + try { + // Loop through all the glyphs, writing the glyph data to the + // in-memory byte stream, collapsing duplicate glyphs, and + // constructing index information. + for (Glyph glyph : glyphs.values()) { + CharDef charDef = new CharDef(glyph.getWidth(), + glyph.getHeight(), + glyph.getBitmap()); + + if (charDefIndex.containsKey(charDef)) { + // Use already-written glyph. + if (debug) + System.out.printf("Duplicate glyph for character U+%04X%n", + glyph.getCodePoint()); + final int charOffset = charDefIndex.get(charDef).intValue(); + final CharStorageInfo info = + new CharStorageInfo(glyph.getCodePoint(), charOffset); + charIndex.put(glyph.getCodePoint(), info); + } else { + // Write glyph data. + final int charOffset = charDefs.size(); + final CharStorageInfo info = + new CharStorageInfo(glyph.getCodePoint(), charOffset); + charIndex.put(glyph.getCodePoint(), info); + + charDefIndex.put(charDef, (long) charOffset); + + charDefs.writeShort(glyph.getWidth()); + charDefs.writeShort(glyph.getHeight()); + charDefs.writeShort(glyph.getBbox()); + charDefs.writeShort(glyph.getBboy()); + charDefs.writeShort(glyph.getDeviceWidth()); + charDefs.write(glyph.getBitmap()); + } + } + } catch (IOException e) { + throw new RuntimeException("Error writing to in-memory byte stream", e); + } + } + + public int getMaxCharWidth() { + return maxCharWidth; + } + + public int getMaxCharHeight() { + return maxCharHeight; + } + + public HashMap getCharIndex() { + if (charIndex == null) throw new IllegalStateException(); + return charIndex; + } + + public byte[] getDefinitionData() { + if (charDefsData == null) throw new IllegalStateException(); + return charDefsData.toByteArray(); + } + + + private static class CharDef { + private final int width; + private final int height; + private final byte[] data; + + public CharDef(int width, int height, byte[] data) { + this.width = width; + this.height = height; + this.data = data; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CharDef charDef = (CharDef) o; + + if (height != charDef.height) return false; + if (width != charDef.width) return false; + if (!Arrays.equals(data, charDef.data)) return false; + + return true; + } + + public int hashCode() { + int result; + result = width; + result = 31 * result + height; + result = 31 * result + Arrays.hashCode(data); + return result; + } + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,139 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.HashMap; +import java.util.Map; +import javax.swing.*; + +public class CharMatrix extends JComponent { + private Font charFont; + private int zoom = 1; + private int startingChar; + private Map glyphLocations = new HashMap(); + private GlyphSelectionListener glyphSelectionListener; + private Point mouseLocation; + + public interface GlyphSelectionListener { + void glyphSelected(int codePoint); + } + + public CharMatrix() { + addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + int codePoint = getCodePointForGlyphAt(e.getPoint()); + if (codePoint != -1) { + if (glyphSelectionListener != null) + glyphSelectionListener.glyphSelected(codePoint); + } + } + + public void mouseExited(MouseEvent e) { + mouseLocation = null; + repaint(); + } + }); + + addMouseMotionListener(new MouseMotionAdapter() { + public void mouseMoved(MouseEvent e) { + mouseLocation = e.getPoint(); + repaint(); + } + }); + } + + public void setGlyphSelectionListener(GlyphSelectionListener listener) { + this.glyphSelectionListener = listener; + } + + public Font getCharFont() { + return charFont; + } + + public void setCharFont(Font font) { + this.charFont = font; + repaint(); + } + + public int getZoom() { + return zoom; + } + + public void setZoom(int zoom) { + this.zoom = zoom; + repaint(); + } + + public int getStartingChar() { + return startingChar; + } + + public void setStartingChar(int startingChar) { + this.startingChar = startingChar; + repaint(); + } + + public int getCodePointForGlyphAt(Point p) { + for (Rectangle rectangle : glyphLocations.keySet()) { + if (rectangle.contains(p)) + return glyphLocations.get(rectangle); + } + return -1; + } + + protected void paintComponent(Graphics g) { + glyphLocations.clear(); + + if (charFont == null) + return; + + int w = getWidth(); + int h = getHeight(); + int maxCharWidth = charFont.getMaxCharWidth(); + int maxCharHeight = charFont.getMaxCharHeight(); + int space = 2; + int charsPerRow = w / (maxCharWidth + space); + int rows = h / (maxCharHeight + space); + + Rectangle highlightBounds = null; + int codePoint = startingChar; + int y = 3 + charFont.getAscent(); // Point y to the baseline. + for (int row = 0; row < rows; row++) { + int x = 3; + for (int col = 0; col < charsPerRow; col++) { + Glyph glyph = charFont.getGlyph(codePoint); + + if (glyph != null) { + Rectangle charBounds = + CharView.drawGlyph(g, charFont, glyph, x, y, zoom, getForeground()); + // Expand the bounding box for a bigger clickable area. + final int boundsPad = 1; + charBounds.x -= boundsPad; + charBounds.y -= boundsPad; + charBounds.width += 2 * boundsPad; + charBounds.height += 2 * boundsPad; + glyphLocations.put(charBounds, codePoint); + if (mouseLocation != null && charBounds.contains(mouseLocation)) { + highlightBounds = charBounds; + } + } + x += maxCharWidth + space; + codePoint++; + } + + y += maxCharHeight + space; + } + + if (highlightBounds != null) { + g.setColor(Color.blue); + Graphics2D g2 = (Graphics2D) g; + g2.setStroke(new BasicStroke(3f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 2f)); + g2.draw(new Rectangle(highlightBounds.x - 2, highlightBounds.y - 2, + highlightBounds.width + 3, highlightBounds.height + 3)); + g2.setStroke(new BasicStroke(1f)); + } + + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,19 @@ +package org.gnu.grub.fonttool; + +class CharStorageInfo { + private final int codePoint; + private final int fileOffset; + + public CharStorageInfo(int codePoint, int fileOffset) { + this.codePoint = codePoint; + this.fileOffset = fileOffset; + } + + public int getCodePoint() { + return codePoint; + } + + public int getFileOffset() { + return fileOffset; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharView.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharView.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharView.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,101 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import java.util.Formatter; +import javax.swing.*; + +public class CharView extends JComponent { + private Font charFont = null; + private Glyph glyph = null; + private int zoom = 1; + + public Font getCharFont() { + return charFont; + } + + public void setCharFont(Font charFont) { + this.charFont = charFont; + } + + public Glyph getGlyph() { + return glyph; + } + + public void setGlyph(Glyph glyph) { + this.glyph = glyph; + repaint(); + } + + public void setZoom(int zoom) { + this.zoom = zoom; + } + + public Dimension getPreferredSize() { + if (glyph == null) + return new Dimension(1, 1); + return new Dimension(glyph.getWidth() * zoom, glyph.getHeight() * zoom); + } + + protected void paintComponent(Graphics g) { + if (glyph == null) { + // Draw a gray 'X' indicating no character selected. + g.setColor(Color.gray); + g.drawLine(0, 0, getWidth() - 1, getHeight() - 1); + g.drawLine(getWidth(), 0, 0, getHeight() - 1); + return; + } + + final int x0 = (getWidth() - glyph.getWidth() * zoom) / 2; + final int y0 = getHeight() / 2; + drawGlyph(g, charFont, glyph, x0, y0, zoom, getForeground()); + + g.setFont(new java.awt.Font("SansSerif", java.awt.Font.PLAIN, 12)); + FontMetrics fm = g.getFontMetrics(); + Formatter f = new Formatter(); + f.format("U+%04X", glyph.getCodePoint()); + String str = f.toString(); + g.drawString(str, + (getWidth() - fm.stringWidth(str)) / 2, + getHeight() - fm.getHeight()); + } + + /** + * Draw the specified glyph with its baseline at y0 and the horizontal + * start at x0. + */ + static Rectangle drawGlyph(Graphics g, Font font, Glyph glyph, int startX, int baselineY, int zoom, Color fg) { + // Adjust (x0, y0) so that it puts the bitmap at the right location. + final int offsetX = zoom * glyph.getBbox(); + final int offsetY = zoom * glyph.getBboy(); + final int bitmapLeft = startX + offsetX; + final int bitmapBottom = baselineY - offsetY; + final int bitmapTop = bitmapBottom - zoom * glyph.getHeight(); + + // Draw a gray box around the glyph bounds. + g.setColor(Color.lightGray); + g.drawRect(bitmapLeft - 1, bitmapTop - 1, + glyph.getWidth() * zoom + 1, zoom * glyph.getHeight() + 1); + + // Draw the baseline + g.setColor(new Color(110, 110, 255)); + g.drawLine(startX, baselineY, startX + zoom * glyph.getDeviceWidth(), baselineY); + + // Draw the glyph. + g.setColor(fg); + byte[] bitmap = glyph.getBitmap(); + int bitIndex = 0; + for (int y = 0; y < glyph.getHeight(); y++) { + for (int x = 0; x < glyph.getWidth(); x++, bitIndex++) { + int byteIndex = bitIndex / 8; + int bitShift = bitIndex % 8; + byte b = bitmap[byteIndex]; + boolean bitValue = (b & (0x80 >>> bitShift)) != 0; + if (bitValue) { + g.fillRect(bitmapLeft + x * zoom, bitmapTop + y * zoom, zoom, zoom); + } + } + } + + return new Rectangle(bitmapLeft, bitmapTop, zoom * glyph.getWidth(), zoom * glyph.getHeight()); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Converter.java' --- util/fonttool/src/org/gnu/grub/fonttool/Converter.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Converter.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,78 @@ +package org.gnu.grub.fonttool; + +import java.io.IOException; + +/** + * Program to convert BDF fonts into PFF2 fonts for use with GRUB. + */ +public class Converter { + public static void main(String[] args) { + if (args.length < 1) { + printUsageAndExit(); + } + + String in = null; + String out = null; + + try { + for (String arg : args) { + if (arg.startsWith("--")) { + String option; + String value; + int equalsPos = arg.indexOf('='); + if (equalsPos < 0) { + option = arg.substring(2); + value = null; + } else { + option = arg.substring(2, equalsPos); + value = arg.substring(equalsPos + 1); + } + + if ("in".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + in = value; + } else if ("out".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + out = value; + } + } else { + throw new CommandLineException("Non-option argument `" + arg + "'."); + } + } + if (in == null || out == null) { + throw new CommandLineException("Both --in=X and --out=Y must be specified."); + } + } catch (CommandLineException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + + try { + // Read BDF. + Font font = BDFLoader.loadFontFile(in); + + // Write PFF2. + new PFF2Writer(out).writeFont(font); + } catch (IOException e) { + System.err.println("I/O error converting font: " + e); + e.printStackTrace(); + System.exit(1); + } + } + + private static class CommandLineException extends Exception { + public CommandLineException(String message) { + super(message); + } + } + + private static void printUsageAndExit() { + System.err.println("GNU GRUB Font Conversion Tool"); + System.err.println(); + System.err.println("Usage: Converter --in=IN.bdf --out=OUT.pf2"); + System.err.println(); + System.exit(1); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java' --- util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,9 @@ +package org.gnu.grub.fonttool; + +import java.io.IOException; + +public class FileFormatException extends IOException { + public FileFormatException(String msg) { + super(msg); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Font.java' --- util/fonttool/src/org/gnu/grub/fonttool/Font.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Font.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,106 @@ +package org.gnu.grub.fonttool; + +import java.util.TreeMap; + +public class Font { + public static final int UNKNOWN_POINT_SIZE = -1; + + private TreeMap glyphs; + private String family; + private boolean bold; + private boolean italic; + private int pointSize; + private int maxCharWidth; + private int maxCharHeight; + private int ascent; + private int descent; + + public Font() { + glyphs = new TreeMap(); + } + + public String getFamily() { + return family; + } + + public void setFamily(String family) { + this.family = family; + } + + public boolean isBold() { + return bold; + } + + public void setBold(boolean bold) { + this.bold = bold; + } + + public boolean isItalic() { + return italic; + } + + public void setItalic(boolean italic) { + this.italic = italic; + } + + public int getPointSize() { + return pointSize; + } + + public void setPointSize(int pointSize) { + this.pointSize = pointSize; + } + + public int getMaxCharWidth() { + return maxCharWidth; + } + + public void setMaxCharWidth(int maxCharWidth) { + this.maxCharWidth = maxCharWidth; + } + + public int getMaxCharHeight() { + return maxCharHeight; + } + + public void setMaxCharHeight(int maxCharHeight) { + this.maxCharHeight = maxCharHeight; + } + + public int getAscent() { + return ascent; + } + + public void setAscent(int ascent) { + this.ascent = ascent; + } + + public int getDescent() { + return descent; + } + + public void setDescent(int descent) { + this.descent = descent; + } + + public void putGlyph(int codePoint, Glyph glyph) { + glyphs.put(codePoint, glyph); + } + + public TreeMap getGlyphs() { + return glyphs; + } + + public Glyph getGlyph(int codePoint) { + return glyphs.get(codePoint); + } + + public String getStandardName() { + StringBuilder name = new StringBuilder(getFamily()); + if (isBold()) name.append(" Bold"); + if (isItalic()) name.append(" Italic"); + name.append(' '); + name.append(getPointSize()); + return name.toString(); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Glyph.java' --- util/fonttool/src/org/gnu/grub/fonttool/Glyph.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Glyph.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,83 @@ +package org.gnu.grub.fonttool; + +public class Glyph { + private final int codePoint; + private final int width; + private final int height; + + // These define the amounts to shift the character bitmap by + // before drawing it. See + // http://www.adobe.com/devnet/font/pdfs/5005.BDF_Spec.pdf + // and + // http://www.linuxbabble.com/documentation/x/bdf/ + // for explanatory figures. + private final int bbox; + private final int bboy; + + // Number of pixels to advance horizontally from this character's origin + // to the origin of the next character. + private final int deviceWidth; + + // Row-major order, no padding. Rows can break within a byte. + // MSb is first (leftmost/uppermost) pixel. + private final byte[] bitmap; + + public Glyph(int codePoint, int width, int height, int bbox, int bboy, int deviceWidth) { + this(codePoint, width, height, bbox, bboy, deviceWidth, + new byte[(width * height + 7) / 8]); + } + + public Glyph(int codePoint, int width, int height, + int bbox, int bboy, int deviceWidth, + byte[] bitmap) { + this.codePoint = codePoint; + this.width = width; + this.height = height; + this.bboy = bboy; + this.bbox = bbox; + this.deviceWidth = deviceWidth; + this.bitmap = bitmap; + } + + public void setPixel(int x, int y, boolean value) { + if (x < 0 || y < 0 || x >= width || y >= height) + throw new IllegalArgumentException( + "Invalid pixel location (" + x + ", " + y + ") for " + + width + "x" + height + " glyph"); + + int bitIndex = y * width + x; + int byteIndex = bitIndex / 8; + int bitPos = bitIndex % 8; + int v = value ? 0x80 >>> bitPos : 0; + int mask = ~(0x80 >>> bitPos); + bitmap[byteIndex] = (byte) ((bitmap[byteIndex] & mask) | v); + } + + public int getCodePoint() { + return codePoint; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getBbox() { + return bbox; + } + + public int getBboy() { + return bboy; + } + + public int getDeviceWidth() { + return deviceWidth; + } + + public byte[] getBitmap() { + return bitmap; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,203 @@ +package org.gnu.grub.fonttool; + +import java.io.EOFException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; + +/** + * Load a PFF2 font completely into memory as an instance of Font. + */ +public class PFF2Loader { + private static final int debug + = Integer.parseInt(System.getProperty("fonttool.debug", "0")); + + private RandomAccessFile f; + private Font font; + + private PFF2Loader(String filename) throws FileNotFoundException { + f = new RandomAccessFile(filename, "r"); + font = new Font(); + } + + public static boolean isPFF2File(String filename) { + RandomAccessFile f = null; + boolean isFont = false; + try { + f = new RandomAccessFile(filename, "r"); + byte[] sectionNameBytes = new byte[4]; + f.readFully(sectionNameBytes); + String sectionName = new String(sectionNameBytes, "US-ASCII"); + if (sectionName.equals(PFF2Sections.FILE)) { + final String expectedType = "PFF2"; + int len = f.readInt(); + if (len == expectedType.length()) { + byte[] typeBytes = new byte[len]; + f.readFully(typeBytes); + String type = new String(typeBytes, "US-ASCII"); + if (type.equals(expectedType)) { + isFont = true; + } + } + } + f.close(); + } catch (IOException e) { + if (f != null) { + try { + f.close(); + } catch (IOException e1) { + // Ignore. + } + } + } + + return isFont; + } + + public static Font loadFontFile(String filename) throws IOException { + PFF2Loader loader = new PFF2Loader(filename); + loader.loadFont(); + return loader.getFont(); + } + + private void loadFont() throws IOException { + String s; + s = openSection(); + if (!PFF2Sections.FILE.equals(s)) + throw new FileFormatException( + "First PFF2 section must be FILE but is `" + s + "'."); + + s = readCompleteSectionAsString(); + if (!"PFF2".equals(s)) + throw new FileFormatException("Unsupported file type `" + s + "'."); + + font.setFamily("Unnamed"); + font.setPointSize(Font.UNKNOWN_POINT_SIZE); + try { + if (debug >= 1) System.out.println("Loading font information..."); + boolean doneLoadingInfo = false; + while (!doneLoadingInfo) { + s = openSection(); + int sectionLength = f.readInt(); + if (debug >= 2) + System.out.println("Section: " + s + " length: " + sectionLength); + if (s.equals(PFF2Sections.MAX_CHAR_WIDTH)) { + int maxCharWidth = readSectionContentsAsUShort(sectionLength); + font.setMaxCharWidth(maxCharWidth); + } else if (s.equals(PFF2Sections.MAX_CHAR_HEIGHT)) { + int maxCharHeight = readSectionContentsAsUShort(sectionLength); + font.setMaxCharHeight(maxCharHeight); + } else if (s.equals(PFF2Sections.FONT_POINT_SIZE)) { + int pointSize = readSectionContentsAsUShort(sectionLength); + font.setPointSize(pointSize); + } else if (s.equals(PFF2Sections.FONT_ASCENT)) { + int ascent = readSectionContentsAsUShort(sectionLength); + font.setAscent(ascent); + } else if (s.equals(PFF2Sections.FONT_DESCENT)) { + int descent = readSectionContentsAsUShort(sectionLength); + font.setDescent(descent); + } else if (s.equals(PFF2Sections.FONT_NAME)) { + String fontName = readSectionContentsAsString(sectionLength); + if (debug >= 1) + System.out.println("Loaded font: " + fontName); + } else if (s.equals(PFF2Sections.FONT_FAMILY)) { + font.setFamily(readSectionContentsAsString(sectionLength)); + } else if (s.equals(PFF2Sections.FONT_WEIGHT)) { + String weightString = readSectionContentsAsString(sectionLength); + font.setBold(weightString.equalsIgnoreCase("bold")); + } else if (s.equals(PFF2Sections.FONT_SLANT)) { + String slantString = readSectionContentsAsString(sectionLength); + font.setItalic(slantString.equalsIgnoreCase("italic")); + } else if (s.equals(PFF2Sections.CHAR_INDEX)) { + loadCharsFromIndex(sectionLength); + } else if (s.equals(PFF2Sections.REMAINDER_IS_DATA)) { + doneLoadingInfo = true; + } else { + // If the section is not handled, then we just skip it. + if (f.skipBytes(sectionLength) != sectionLength) { + System.err.println("Warning: Could not skip " + + sectionLength + + " bytes for section `" + s + "'."); + } + } + } + } catch (EOFException eofException) { + // End of file; done reading font. + } + f.close(); + } + + private void loadCharsFromIndex(int sectionLength) throws IOException { + long indexSectionEnd = f.getFilePointer() + sectionLength; + try { + while (f.getFilePointer() < indexSectionEnd) { + int codePoint = f.readInt(); + int storageFlags = f.readUnsignedByte(); + int offset = f.readInt(); + + if ((storageFlags & 0x07) == 0) { + Glyph g = loadUncompressedGlyph(codePoint, offset); + font.putGlyph(codePoint, g); + } + } + } catch (EOFException eofException) { + // End of file; simple return. + } + } + + private Glyph loadUncompressedGlyph(int codePoint, int fileOffset) throws IOException { + if (debug >= 3) + System.out.printf("Loading uncompressed glyph U+%04X at %d%n", codePoint, fileOffset); + + long savedPos = f.getFilePointer(); + + f.seek(fileOffset); + int width = f.readUnsignedShort(); + int height = f.readUnsignedShort(); + int offsetX = f.readShort(); + int offsetY = f.readShort(); + int deviceWidth = f.readShort(); + if (debug >= 3) System.out.println(" Size=" + width + "x" + height + + " offset=(" + offsetX + ", " + offsetY + + ") dWidth=" + deviceWidth); + byte[] bitmap = new byte[(width * height + 7) / 8]; + f.readFully(bitmap); + + f.seek(savedPos); + return new Glyph(codePoint, width, height, + offsetX, offsetY, deviceWidth, bitmap); + } + + private int readSectionContentsAsUShort(int sectionLength) throws IOException { + if (sectionLength != 2) + throw new FileFormatException( + "For uint16 section, expected 2 bytes but length is " + + sectionLength + "."); + return f.readUnsignedShort(); + } + + private String openSection() throws IOException { + byte[] b = new byte[4]; + f.readFully(b); + return new String(b, Charset.forName("US-ASCII")); + } + + private String readCompleteSectionAsString() throws IOException { + int n = f.readInt(); + return readSectionContentsAsString(n); + } + + private String readSectionContentsAsString(int n) throws IOException { + StringBuilder buf = new StringBuilder(); + while (n-- != 0) { + char c = (char) (f.readByte() & 0xFF); + buf.append(c); + } + return buf.toString(); + } + + public Font getFont() { + return font; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,19 @@ +package org.gnu.grub.fonttool; + +/** + * Section name constants for the PFF2 file format. + */ +public class PFF2Sections { + static final String FILE = "FILE"; + static final String FONT_NAME = "NAME"; + static final String FONT_FAMILY = "FAMI"; + static final String FONT_WEIGHT = "WEIG"; + static final String FONT_SLANT = "SLAN"; + static final String FONT_POINT_SIZE = "PTSZ"; + static final String MAX_CHAR_WIDTH = "MAXW"; + static final String MAX_CHAR_HEIGHT = "MAXH"; + static final String FONT_ASCENT = "ASCE"; + static final String FONT_DESCENT = "DESC"; + static final String CHAR_INDEX = "CHIX"; + static final String REMAINDER_IS_DATA = "DATA"; +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,133 @@ +package org.gnu.grub.fonttool; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +// TODO Add DEFLATE compressed blocks of characters. +public class PFF2Writer { + private RandomAccessFile f; + private String currentSection; + private long currentSectionStart; + + public PFF2Writer(String filename) throws FileNotFoundException { + this.f = new RandomAccessFile(filename, "rw"); + this.currentSection = null; + this.currentSectionStart = -1; + } + + public void writeFont(Font font) throws IOException { + // Write file type ID header. + writeSection(PFF2Sections.FILE, "PFF2"); + + writeSection(PFF2Sections.FONT_NAME, font.getStandardName()); + writeSection(PFF2Sections.FONT_FAMILY, font.getFamily()); + writeSection(PFF2Sections.FONT_WEIGHT, font.isBold() ? "bold" : "normal"); + writeSection(PFF2Sections.FONT_SLANT, font.isItalic() ? "italic" : "normal"); + if (font.getPointSize() != Font.UNKNOWN_POINT_SIZE) + writeShortSection(PFF2Sections.FONT_POINT_SIZE, font.getPointSize()); + + // Construct character definitions. + CharDefs charDefs = new CharDefs(font); + charDefs.buildDefinitions(); + + // Write max character width and height metrics. + writeShortSection(PFF2Sections.MAX_CHAR_WIDTH, charDefs.getMaxCharWidth()); + writeShortSection(PFF2Sections.MAX_CHAR_HEIGHT, charDefs.getMaxCharHeight()); + writeShortSection(PFF2Sections.FONT_ASCENT, font.getAscent()); + writeShortSection(PFF2Sections.FONT_DESCENT, font.getDescent()); + + // Write character index with pointers to the character definitions. + beginSection(PFF2Sections.CHAR_INDEX); + + // Determine the size of the index, so we can properly refer to the + // character definition offset in the index. The actual number of + // bytes written is compared to the calculated value to ensure we + // are correct. + final int indexStart = (int) f.getFilePointer(); + final int calculatedIndexLength = + font.getGlyphs().size() * (4 + 1 + 4); + final int charDefStart = indexStart + calculatedIndexLength + 8; + + for (CharStorageInfo storageInfo : charDefs.getCharIndex().values()) { + f.writeInt(storageInfo.getCodePoint()); + f.writeByte(0); // Storage flags: bits 1..0 = 00b : uncompressed. + f.writeInt(charDefStart + storageInfo.getFileOffset()); + } + + final int indexEnd = (int) f.getFilePointer(); + if (indexEnd - indexStart != calculatedIndexLength) { + throw new RuntimeException("Incorrect index length calculated, calc=" + + calculatedIndexLength + + " actual=" + (indexEnd - indexStart)); + } + endSection(PFF2Sections.CHAR_INDEX); + + f.writeBytes(PFF2Sections.REMAINDER_IS_DATA); + f.writeInt(-1); // Data takes up the rest of the file. + f.write(charDefs.getDefinitionData()); + + f.close(); + } + + private void beginSection(String sectionName) throws IOException { + verifyOkToBeginSection(sectionName); + + f.writeBytes(sectionName); + f.writeInt(-1); // Placeholder for the section length. + currentSection = sectionName; + currentSectionStart = f.getFilePointer(); + } + + private void endSection(String sectionName) throws IOException { + verifyOkToEndSection(sectionName); + + long sectionEnd = f.getFilePointer(); + long sectionLength = sectionEnd - currentSectionStart; + f.seek(currentSectionStart - 4); + f.writeInt((int) sectionLength); + f.seek(sectionEnd); + currentSection = null; + currentSectionStart = -1; + } + + private void verifyOkToBeginSection(String sectionName) { + if (sectionName.length() != 4) + throw new IllegalArgumentException( + "Section names must be 4 characters: `" + sectionName + "'."); + if (currentSection != null) + throw new IllegalStateException( + "Attempt to start `" + sectionName + + "' section before ending the previous section `" + + currentSection + "'."); + } + + private void verifyOkToEndSection(String sectionName) { + if (sectionName.length() != 4) + throw new IllegalStateException("Invalid section name '" + sectionName + + "'; must be 4 characters."); + if (currentSection == null) + throw new IllegalStateException( + "Attempt to end section `" + sectionName + + "' when no section active."); + if (!sectionName.equals(currentSection)) + throw new IllegalStateException( + "Attempt to end `" + sectionName + + "' section during active section `" + + currentSection + "'."); + } + + private void writeSection(String sectionName, String contents) throws IOException { + verifyOkToBeginSection(sectionName); + f.writeBytes(sectionName); + f.writeInt(contents.length()); + f.writeBytes(contents); + } + + private void writeShortSection(String sectionName, int value) throws IOException { + verifyOkToBeginSection(sectionName); + f.writeBytes(sectionName); + f.writeInt(2); + f.writeShort(value); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Viewer.java' --- util/fonttool/src/org/gnu/grub/fonttool/Viewer.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Viewer.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,135 @@ +package org.gnu.grub.fonttool; + +import java.io.File; +import java.io.IOException; +import javax.swing.*; + +/** + * Program to view fonts. + *

+ * Supports PFF2 and BDF format font files. + */ +public class Viewer { + public static void main(String[] args) { + if (args.length < 1) { + printUsageAndExit(); + } + + boolean verbose = false; + String format = null; + String filename = null; + + try { + for (String arg : args) { + if (arg.startsWith("--")) { + String option; + String value; + int equalsPos = arg.indexOf('='); + if (equalsPos < 0) { + option = arg.substring(2); + value = null; + } else { + option = arg.substring(2, equalsPos); + value = arg.substring(equalsPos + 1); + } + + if ("format".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + format = value; + } else if ("verbose".equals(option)) { + verbose = true; + } + } else { + if (filename == null) { + filename = arg; + } else { + throw new CommandLineException( + "Extra argument after filename: `" + arg + "'."); + } + } + } + if (filename == null) { + throw new CommandLineException("A filename must be specified."); + } + if (!(format == null || "bdf".equals(format) || "pff2".equals(format))) { + throw new CommandLineException("Invalid format: must be `bdf' or `pff2'."); + } + } catch (CommandLineException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + + if (!new File(filename).exists()) { + System.err.println("Error: File `" + filename + "' not found."); + System.exit(1); + } + + if (format == null) { + // Autodetect the format. + if (PFF2Loader.isPFF2File(filename)) + format = "pff2"; + else if (BDFLoader.isBDFFile(filename)) + format = "bdf"; + else { + System.err.println("Error: Unable to detect font file format " + + "for `" + filename + "'."); + System.exit(1); + } + if (verbose) + System.out.println("Detected file format `" + format + "'."); + } + + Font font = null; + try { + if ("bdf".equals(format)) { + if (verbose) + System.out.println("Loading BDF font."); + font = BDFLoader.loadFontFile(filename); + } else if ("pff2".equals(format)) { + if (verbose) + System.out.println("Loading PFF2 font."); + font = PFF2Loader.loadFontFile(filename); + } else { + System.err.println("Error: Bad format `" + format + "'"); + System.exit(1); + } + } catch (IOException e) { + System.err.println("I/O error loading font: " + e); + e.printStackTrace(); + System.exit(1); + } + + if (font == null) { + System.err.println("Error: Font not loaded."); + System.exit(1); + } + + if (verbose) { + System.out.println("Loaded font: '" + font.getStandardName() + "'"); + } + + ViewerFrame f = new ViewerFrame(font); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.setLocationRelativeTo(null); // Center the frame on screen. + f.setVisible(true); + } + + private static class CommandLineException extends Exception { + public CommandLineException(String message) { + super(message); + } + } + + private static void printUsageAndExit() { + System.err.println("GNU GRUB Font Viewer"); + System.err.println(); + System.err.println("Usage: Viewer [--format=FORMAT] FILE"); + System.err.println(" --format=bdf The font is in BDF format."); + System.err.println(" --format=pff2 The font is in PFF2 format."); + System.err.println(" If no --format option is given, the type will be"); + System.err.println(" automatically detected."); + System.err.println(); + System.exit(1); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java' --- util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,61 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import javax.swing.*; +import javax.swing.border.BevelBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +public class ViewerFrame extends JFrame { + private final Font font; + + private JLabel startingCodePointLabel; + private JSlider startingCodePointSlider; + private CharMatrix charMatrix; + private CharView charView; + private JLabel statusBar; + + public ViewerFrame(Font font) { + this.font = font; + + createComponents(); + buildUI(); + setSize(800, 600); + } + + private void createComponents() { + startingCodePointLabel = new JLabel("Starting Code Point"); + startingCodePointSlider = new JSlider(JSlider.HORIZONTAL, 0, 65535, 0); + startingCodePointSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + charMatrix.setStartingChar(startingCodePointSlider.getValue()); + } + }); + charMatrix = new CharMatrix(); + charMatrix.setCharFont(font); + charMatrix.setGlyphSelectionListener(new CharMatrix.GlyphSelectionListener() { + public void glyphSelected(int codePoint) { + charView.setGlyph(font.getGlyph(codePoint)); + } + }); + charView = new CharView(); + charView.setZoom(3); + charView.setBorder(new BevelBorder(BevelBorder.RAISED)); + statusBar = new JLabel(); + } + + private void buildUI() { + JComponent topPanel = Box.createVerticalBox(); + topPanel.add(startingCodePointLabel); + topPanel.add(startingCodePointSlider); + + JComponent charPanel = new JPanel(new GridLayout(2, 1)); + charPanel.add(charMatrix); + charPanel.add(charView); + + setLayout(new BorderLayout()); + add(topPanel, BorderLayout.NORTH); + add(charPanel, BorderLayout.CENTER); + add(statusBar, BorderLayout.SOUTH); + } +} === added directory 'util/fonttool/test' === added directory 'util/fonttool/test/org' === added directory 'util/fonttool/test/org/gnu' === added directory 'util/fonttool/test/org/gnu/grub' === added directory 'util/fonttool/test/org/gnu/grub/fonttool' === added file 'util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java' --- util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,110 @@ +package org.gnu.grub.fonttool; + +import org.junit.Assert; +import org.junit.Test; + +public class TestGlyph { + @Test + public void testConstructor3x5() { + Glyph g = new Glyph(65, 3, 5, 0, 0, 0); + + // Should be empty upon construction. + assertBitmapEquals("..." + + "..." + + "..." + + "..." + + "...", + g.getBitmap()); + + // Should have ceil(3 * 5 / 8) = ceil(15 / 8) = ceil(1 + 7/8) = 2 bytes. + Assert.assertEquals("Wrong bitmap array size.", + 2, g.getBitmap().length); + } + + @Test + public void testConstructor5x5() { + Glyph g = new Glyph(65, 5, 5, 0, 0, 0); + + // Should be empty upon construction. + assertBitmapEquals("....." + + "....." + + "....." + + "....." + + ".....", + g.getBitmap()); + + // Should have ceil(5 * 5 / 8) = ceil(25 / 8) = ceil(3 + 1/8) = 4 bytes. + Assert.assertEquals("Wrong bitmap array size.", + 4, g.getBitmap().length); + } + + @Test + public void testSetPixel() { + Glyph g = new Glyph(65, 3, 5, 0, 0, 0); + + g.setPixel(0, 0, true); + Assert.assertEquals(0x80, g.getBitmap()[0] & 0xFF); + Assert.assertEquals(0x00, g.getBitmap()[1] & 0xFF); + assertBitmapEquals("X.." + + "..." + + "..." + + "..." + + "...", + g.getBitmap()); + + g.setPixel(2, 1, true); + Assert.assertEquals(0x84, g.getBitmap()[0] & 0xFF); + Assert.assertEquals(0x00, g.getBitmap()[1] & 0xFF); + assertBitmapEquals("X.." + + "..X" + + "..." + + "..." + + "...", + g.getBitmap()); + } + + static void assertBitmapEquals(String expectedBitmap, byte[] actualBitmap) { + Assert.assertNotNull("expected bitmap null", expectedBitmap); + Assert.assertNotNull("actual bitmap null", actualBitmap); + StringBuilder errors = new StringBuilder(); + + int bit = 7; + int byteIndex = -1; + for (int bitIndex = 0; bitIndex < expectedBitmap.length(); bitIndex++) { + char c = expectedBitmap.charAt(bitIndex); + boolean expectedBit; + if (c == '.') { + expectedBit = false; + } else if (c == 'X') { + expectedBit = true; + } else { + throw new IllegalArgumentException("Bad character '" + c + "' in expected bitmap string"); + } + + byteIndex = bitIndex / 8; + byte b = actualBitmap[byteIndex]; + boolean actualBit = (b & (1 << bit)) != 0; + if (actualBit != expectedBit) { + errors.append("[").append(bitIndex) + .append("] wrong (it is ") + .append(actualBit ? "1" : "0").append("). "); + } + + if (bit == 0) { + bit = 7; + } else { + bit--; + } + } + + if (byteIndex != expectedBitmap.length() / 8) { + errors.append("Last byte index wrong, was ") + .append(byteIndex).append(" but expected ") + .append(expectedBitmap.length() - 1); + } + + if (errors.length() != 0) { + Assert.fail("Bitmap wrong: " + errors); + } + } +} === modified file 'video/bitmap.c' --- video/bitmap.c 2007-07-21 22:32:33 +0000 +++ video/bitmap.c 2008-07-03 13:49:18 +0000 @@ -78,7 +78,7 @@ switch (blit_format) { - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8: + case GRUB_VIDEO_BLIT_FORMAT_RGBA_8888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB | GRUB_VIDEO_MODE_TYPE_ALPHA; mode_info->bpp = 32; @@ -94,7 +94,7 @@ mode_info->reserved_field_pos = 24; break; - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8: + case GRUB_VIDEO_BLIT_FORMAT_RGB_888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB; mode_info->bpp = 24; mode_info->bytes_per_pixel = 3; === added file 'video/bitmap_scale.c' --- video/bitmap_scale.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,99 @@ +/* bitmap_scale.c - Bitmap scaling interface. */ +/* + * 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 . + */ + +#include +#include +#include +#include +#include +#include + +/* + * This function creates a new scaled version of the bitmap SRC. The new + * bitmap has dimensions DST_WIDTH by DST_HEIGHT. The scaling algorithm + * is given by SCALE_METHOD. If an error is encountered, the return code is + * not equal to GRUB_ERR_NONE, and the bitmap DST is either not created, or + * it is destroyed before this function returns. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum grub_video_bitmap_scale_method + scale_method) +{ + + /* Verify the simplifying assumptions. */ + if (src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "null src bitmap in grub_video_bitmap_create_scaled"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "src format not supported for scale"); + if (src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + if (dst_width <= 0 || dst_height <= 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "requested to scale to a size w/ a zero dimension"); + if (src->mode_info.bytes_per_pixel * 8 != src->mode_info.bpp) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "bitmap to scale has inconsistent Bpp and bpp"); + + /* Create the new bitmap. */ + grub_err_t ret; + ret = grub_video_bitmap_create (dst, dst_width, dst_height, + src->mode_info.blit_format); + if (ret != GRUB_ERR_NONE) + return ret; /* Error. */ + + switch (scale_method) + { + case GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST: + ret = grub_video_bitmap_scale_nn (*dst, src); + break; + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR: + ret = grub_video_bitmap_scale_bilinear (*dst, src); + break; + default: + ret = grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid scale_method value"); + break; + } + + if (ret == GRUB_ERR_NONE) + { + /* Success: *dst is now a pointer to the scaled bitmap. */ + return GRUB_ERR_NONE; + } + else + { + /* Destroy the bitmap and return the error code. */ + grub_video_bitmap_destroy (*dst); + return ret; + } +} === added file 'video/bitmap_scale_bilinear.c' --- video/bitmap_scale_bilinear.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_bilinear.c 2008-07-03 14:28:15 +0000 @@ -0,0 +1,145 @@ +/* bitmap_scale_bilinear.c - Bilinear image scaling algorithm. */ +/* + * 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 . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the bilinear interpolation algorithm + * to interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale func"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* If we have enough space to do so, use bilinear interpolation. + * Otherwise, fall back to nearest neighbor for this pixel. */ + if (sx < sw - 1 && sy < sh - 1) + { + /* Do bilinear interpolation. */ + + /* Fixed-point .8 numbers representing the fraction of the + * distance in the x (u) and y (v) direction within the + * box of 4 pixels in the source. */ + int u = (256 * sw * dx / dw) - (sx * 256); + int v = (256 * sh * dy / dh) - (sy * 256); + + for (comp = 0; comp < bytes_per_pixel; comp++) + { + /* Get the component's values for the + * 4 source corner pixels. */ + grub_uint8_t f00 = sptr[comp]; + grub_uint8_t f10 = sptr[comp + bytes_per_pixel]; + grub_uint8_t f01 = sptr[comp + sstride]; + grub_uint8_t f11 = sptr[comp + sstride + bytes_per_pixel]; + + /* Do linear interpolations along the top and bottom + * rows of the box. */ + grub_uint8_t f0y = (256 - v) * f00 / 256 + v * f01 / 256; + grub_uint8_t f1y = (256 - v) * f10 / 256 + v * f11 / 256; + + /* Interpolate vertically. */ + grub_uint8_t fxy = (256 - u) * f0y / 256 + u * f1y / 256; + + dptr[comp] = fxy; + } + } + else + { + /* Fall back to nearest neighbor interpolation. */ + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + } + return GRUB_ERR_NONE; +} === added file 'video/bitmap_scale_nn.c' --- video/bitmap_scale_nn.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_nn.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,109 @@ +/* bitmap_scale_nn.c - Nearest neighbor image scaling algorithm. */ +/* + * 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 . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the nearest neighbor algorithm to + * interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale_nn"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + return GRUB_ERR_NONE; +} === modified file 'video/i386/pc/vbe.c' --- video/i386/pc/vbe.c 2008-01-21 15:48:27 +0000 +++ video/i386/pc/vbe.c 2008-07-03 14:19:24 +0000 @@ -63,6 +63,10 @@ static struct { struct grub_video_render_target render_target; + int is_double_buffered; /* Is the video mode double buffered? */ + int displayed_page; /* The page # that is the front buffer. */ + int render_page; /* The page # that is the back buffer. */ + grub_size_t page_size; /* The size of a page in bytes. */ unsigned int bytes_per_scan_line; unsigned int bytes_per_pixel; @@ -77,6 +81,8 @@ static grub_uint32_t mode_in_use = 0x55aa; static grub_uint16_t *mode_list; +static void commit_video_page_update (void); + static void * real2pm (grub_vbe_farptr_t ptr) { @@ -239,6 +245,10 @@ /* Just ignore the status. */ } + /* Set up page information. */ + framebuffer.displayed_page = 0; + framebuffer.render_page = 1; + /* Copy mode info for caller. */ if (mode_info) grub_memcpy (mode_info, &active_mode_info, sizeof (*mode_info)); @@ -533,9 +543,18 @@ render_target->viewport.width = active_mode_info.x_resolution; render_target->viewport.height = active_mode_info.y_resolution; - /* Set framebuffer pointer and mark it as non allocated. */ + /* Set up double buffering information. */ + framebuffer.is_double_buffered = + ((mode_type & GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED) != 0); + framebuffer.displayed_page = 0; + framebuffer.render_page = framebuffer.is_double_buffered ? 1 : 0; + framebuffer.page_size = + framebuffer.bytes_per_scan_line * render_target->mode_info.height; + + /* Mark framebuffer memory as non allocated. */ render_target->is_allocated = 0; - render_target->data = framebuffer.ptr; + /* Set the framebuffer memory data pointer and display the right page. */ + commit_video_page_update (); /* Copy default palette to initialize emulated palette. */ for (i = 0; @@ -708,6 +727,16 @@ return minindex; } + else if ((render_target->mode_info.mode_type + & GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP) != 0) + { + if (red == render_target->mode_info.fg_red + && green == render_target->mode_info.fg_green + && blue == render_target->mode_info.fg_blue) + return 1; + else + return 0; + } else { grub_uint32_t value; @@ -737,6 +766,17 @@ /* No alpha available in index color modes, just use same value as in only RGB modes. */ return grub_video_vbe_map_rgb (red, green, blue); + else if ((render_target->mode_info.mode_type + & GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP) != 0) + { + if (red == render_target->mode_info.fg_red + && green == render_target->mode_info.fg_green + && blue == render_target->mode_info.fg_blue + && alpha == render_target->mode_info.fg_alpha) + return 1; + else + return 0; + } else { grub_uint32_t value; @@ -797,6 +837,24 @@ *alpha = framebuffer.palette[color].a; return; } + else if ((mode_info->mode_type + & GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP) != 0) + { + if (color & 1) + { + *red = mode_info->fg_red; + *green = mode_info->fg_green; + *blue = mode_info->fg_blue; + *alpha = mode_info->fg_alpha; + } + else + { + *red = mode_info->bg_red; + *green = mode_info->bg_green; + *blue = mode_info->bg_blue; + *alpha = mode_info->bg_alpha; + } + } else { grub_uint32_t tmp; @@ -876,17 +934,24 @@ target.data = render_target->data; /* Try to figure out more optimized version. */ - if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbefill_R8G8B8A8 (&target, color, x, y, - width, height); - return GRUB_ERR_NONE; - } - - if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) - { - grub_video_i386_vbefill_R8G8B8 (&target, color, x, y, - width, height); + if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbefill_direct32 (&target, color, x, y, + width, height); + return GRUB_ERR_NONE; + } + + if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbefill_direct32 (&target, color, x, y, + width, height); + return GRUB_ERR_NONE; + } + + if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) + { + grub_video_i386_vbefill_direct24 (&target, color, x, y, + width, height); return GRUB_ERR_NONE; } @@ -903,76 +968,6 @@ return GRUB_ERR_NONE; } -// TODO: Remove this method and replace with bitmap based glyphs -static grub_err_t -grub_video_vbe_blit_glyph (struct grub_font_glyph * glyph, - grub_video_color_t color, int x, int y) -{ - struct grub_video_i386_vbeblit_info target; - unsigned int width; - unsigned int charwidth; - unsigned int height; - unsigned int i; - unsigned int j; - unsigned int x_offset = 0; - unsigned int y_offset = 0; - - /* Make sure there is something to do. */ - if (x >= (int)render_target->viewport.width) - return GRUB_ERR_NONE; - - if (y >= (int)render_target->viewport.height) - return GRUB_ERR_NONE; - - /* Calculate glyph dimensions. */ - width = ((glyph->width + 7) / 8) * 8; - charwidth = width; - height = glyph->height; - - if (x + (int)width < 0) - return GRUB_ERR_NONE; - - if (y + (int)height < 0) - return GRUB_ERR_NONE; - - /* Do not allow drawing out of viewport. */ - if (x < 0) - { - width += x; - x_offset = (unsigned int)-x; - x = 0; - } - if (y < 0) - { - height += y; - y_offset = (unsigned int)-y; - y = 0; - } - - if ((x + width) > render_target->viewport.width) - width = render_target->viewport.width - x; - if ((y + height) > render_target->viewport.height) - height = render_target->viewport.height - y; - - /* Add viewport offset. */ - x += render_target->viewport.x; - y += render_target->viewport.y; - - /* Use vbeblit_info to encapsulate rendering. */ - target.mode_info = &render_target->mode_info; - target.data = render_target->data; - - /* Draw glyph. */ - for (j = 0; j < height; j++) - for (i = 0; i < width; i++) - if ((glyph->bitmap[((i + x_offset) / 8) - + (j + y_offset) * (charwidth / 8)] - & (1 << ((charwidth - (i + x_offset) - 1) % 8)))) - set_pixel (&target, x+i, y+j, color); - - return GRUB_ERR_NONE; -} - /* NOTE: This function assumes that given coordinates are within bounds of handled data. */ static void @@ -985,19 +980,27 @@ if (oper == GRUB_VIDEO_BLIT_REPLACE) { /* Try to figure out more optimized version for replace operator. */ - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbeblit_R8G8B8X8_R8G8B8X8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbeblit_direct32_copy (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_BGRX8888_RGBX8888 (target, source, x, y, width, height, offset_x, offset_y); return; } - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) { - grub_video_i386_vbeblit_R8G8B8_R8G8B8X8 (target, source, + grub_video_i386_vbeblit_RGB888_RGBX8888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1005,26 +1008,42 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8X8 (target, source, + grub_video_i386_vbeblit_index_RGBX8888 (target, source, x, y, width, height, offset_x, offset_y); return; } } - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (target, source, - x, y, width, height, - offset_x, offset_y); - return; - } - - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) - { - grub_video_i386_vbeblit_R8G8B8_R8G8B8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_BGRA8888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbeblit_RGBA8888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGR_888) + { + grub_video_i386_vbeblit_BGR888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) + { + grub_video_i386_vbeblit_RGB888_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1032,13 +1051,24 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8 (target, source, + grub_video_i386_vbeblit_index_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; } } + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_direct32_copy (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + } + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) @@ -1057,19 +1087,19 @@ else { /* Try to figure out more optimized blend operator. */ - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) { - grub_video_i386_vbeblit_R8G8B8A8_R8G8B8A8 (target, source, + grub_video_i386_vbeblit_RGBA8888_RGBA8888 (target, source, x, y, width, height, offset_x, offset_y); return; } - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) { - grub_video_i386_vbeblit_R8G8B8_R8G8B8A8 (target, source, + grub_video_i386_vbeblit_RGB888_RGBA8888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1077,26 +1107,26 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8A8 (target, source, + grub_video_i386_vbeblit_index_RGBA8888 (target, source, x, y, width, height, offset_x, offset_y); return; } } - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) { - grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (target, source, + grub_video_i386_vbeblit_RGBA8888_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; } - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) { - grub_video_i386_vbeblit_R8G8B8_R8G8B8 (target, source, + grub_video_i386_vbeblit_RGB888_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1104,7 +1134,7 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8 (target, source, + grub_video_i386_vbeblit_index_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1219,6 +1249,77 @@ return GRUB_ERR_NONE; } +/* + * Draw the specified glyph at (x, y). The y coordinate designates the + * baseline of the character, while the x coordinate designates the left + * side location of the character. + */ +static grub_err_t +grub_video_vbe_blit_glyph (struct grub_font_glyph *glyph, + grub_video_color_t color, + int left_x, int baseline_y) +{ + struct grub_video_bitmap glyph_bitmap; + + /* Don't try to draw empty glyphs (U+0020, etc.). */ + if (glyph->width == 0 || glyph->height == 0) + return GRUB_ERR_NONE; + + glyph_bitmap.mode_info.width = glyph->width; + glyph_bitmap.mode_info.height = glyph->height; + glyph_bitmap.mode_info.mode_type = + (1 << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) + | GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP; + glyph_bitmap.mode_info.blit_format = GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED; + glyph_bitmap.mode_info.bpp = 1; + glyph_bitmap.mode_info.bytes_per_pixel = 0; /* Really 1 bit per pixel. */ + glyph_bitmap.mode_info.pitch = glyph->width; /* Packed densely as bits. */ + glyph_bitmap.mode_info.number_of_colors = 2; + glyph_bitmap.mode_info.bg_red = 0; + glyph_bitmap.mode_info.bg_green = 0; + glyph_bitmap.mode_info.bg_blue = 0; + glyph_bitmap.mode_info.bg_alpha = 0; + grub_video_vbe_unmap_color(color, + &glyph_bitmap.mode_info.fg_red, + &glyph_bitmap.mode_info.fg_green, + &glyph_bitmap.mode_info.fg_blue, + &glyph_bitmap.mode_info.fg_alpha); + glyph_bitmap.data = glyph->bitmap; + + int bitmap_left = left_x + glyph->offset_x; + int bitmap_bottom = baseline_y - glyph->offset_y; + int bitmap_top = bitmap_bottom - glyph->height; + + return grub_video_vbe_blit_bitmap (&glyph_bitmap, GRUB_VIDEO_BLIT_BLEND, + bitmap_left, bitmap_top, + 0, 0, + glyph->width, glyph->height); +} + +static grub_err_t +grub_video_vbe_draw_string (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y) +{ + grub_size_t len; + grub_size_t i; + int x; + struct grub_font_glyph *glyph; + + len = grub_strlen (str); + x = left_x; + for (i = 0; i < len; i++) + { + glyph = grub_font_get_glyph (font, str[i]); + if (grub_video_vbe_blit_glyph (glyph, color, x, baseline_y) + != GRUB_ERR_NONE) + return grub_errno; + x += glyph->device_width; + } + + return GRUB_ERR_NONE; +} + static grub_err_t grub_video_vbe_blit_render_target (struct grub_video_render_target *source, enum grub_video_blit_operators oper, @@ -1403,10 +1504,36 @@ return GRUB_ERR_NONE; } -static grub_err_t +/* Set framebuffer render target page and display the proper page, based on + * `framebuffer.render_page' and `framebuffer.displayed_page', respectively. */ +static void +commit_video_page_update (void) +{ + /* Set the render target's data pointer to the start of the render_page. */ + framebuffer.render_target.data = + ((char *) framebuffer.ptr) + + framebuffer.page_size * framebuffer.render_page; + + /* Tell the video adapter to display the new front page. */ + int display_start_line = + framebuffer.render_target.mode_info.height + * framebuffer.displayed_page; + grub_vbe_bios_set_display_start (0, display_start_line); +} + +static grub_err_t grub_video_vbe_swap_buffers (void) { - /* TODO: Implement buffer swapping. */ + if (framebuffer.is_double_buffered) + { + /* Swap the page numbers in the framebuffer struct. */ + int new_displayed_page = framebuffer.render_page; + framebuffer.render_page = framebuffer.displayed_page; + framebuffer.displayed_page = new_displayed_page; + + commit_video_page_update (); + } + return GRUB_ERR_NONE; } @@ -1505,17 +1632,13 @@ static grub_err_t grub_video_vbe_set_active_render_target (struct grub_video_render_target *target) { - if (target == GRUB_VIDEO_RENDER_TARGET_FRONT_BUFFER) + if (target == GRUB_VIDEO_RENDER_TARGET_DISPLAY) { render_target = &framebuffer.render_target; - + return GRUB_ERR_NONE; } - if (target == GRUB_VIDEO_RENDER_TARGET_BACK_BUFFER) - return grub_error (GRUB_ERR_NOT_IMPLEMENTED_YET, - "double buffering not implemented yet."); - if (! target->data) return grub_error (GRUB_ERR_BAD_ARGUMENT, "invalid render target given."); @@ -1551,6 +1674,7 @@ .unmap_color = grub_video_vbe_unmap_color, .fill_rect = grub_video_vbe_fill_rect, .blit_glyph = grub_video_vbe_blit_glyph, + .draw_string = grub_video_vbe_draw_string, .blit_bitmap = grub_video_vbe_blit_bitmap, .blit_render_target = grub_video_vbe_blit_render_target, .scroll = grub_video_vbe_scroll, === modified file 'video/i386/pc/vbeblit.c' --- video/i386/pc/vbeblit.c 2008-01-01 12:02:07 +0000 +++ video/i386/pc/vbeblit.c 2008-07-03 13:49:18 +0000 @@ -35,7 +35,133 @@ #include void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_BGRX8888_RGBX8888 (struct grub_video_i386_vbeblit_info + *dst, + struct grub_video_i386_vbeblit_info + *src, int x, int y, int width, + int height, int offset_x, + int offset_y) +{ + int i; + int j; + grub_uint8_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint8_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint8_t r = *srcptr++; + grub_uint8_t g = *srcptr++; + grub_uint8_t b = *srcptr++; + grub_uint8_t a = *srcptr++; + + *dstptr++ = b; + *dstptr++ = g; + *dstptr++ = r; + *dstptr++ = a; + } + srcptr += srcrowskip; + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_BGRA8888_RGB888 (struct grub_video_i386_vbeblit_info + *dst, + struct grub_video_i386_vbeblit_info + *src, int x, int y, int width, + int height, int offset_x, + int offset_y) +{ + int i; + int j; + grub_uint8_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint8_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint8_t r = *srcptr++; + grub_uint8_t g = *srcptr++; + grub_uint8_t b = *srcptr++; + + *dstptr++ = b; + *dstptr++ = g; + *dstptr++ = r; + *dstptr++ = 255; /* Alpha component: Set opaque. */ + } + srcptr += srcrowskip; + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_BGR888_RGB888 (struct grub_video_i386_vbeblit_info + *dst, + struct grub_video_i386_vbeblit_info + *src, int x, int y, int width, + int height, int offset_x, + int offset_y) +{ + int i; + int j; + grub_uint8_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint8_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint8_t r = *srcptr++; + grub_uint8_t g = *srcptr++; + grub_uint8_t b = *srcptr++; + + *dstptr++ = b; + *dstptr++ = g; + *dstptr++ = r; + } + srcptr += srcrowskip; + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_RGBA8888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -101,10 +227,10 @@ } void -grub_video_i386_vbeblit_R8G8B8X8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y) +grub_video_i386_vbeblit_direct32_copy (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y) { int j; grub_uint32_t *srcptr; @@ -126,7 +252,7 @@ } void -grub_video_i386_vbeblit_R8G8B8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGB888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -193,7 +319,7 @@ } void -grub_video_i386_vbeblit_R8G8B8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGB888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -231,7 +357,7 @@ } void -grub_video_i386_vbeblit_index_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_index_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -295,7 +421,7 @@ } void -grub_video_i386_vbeblit_index_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_index_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -332,7 +458,7 @@ } void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGBA8888_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -368,7 +494,7 @@ } void -grub_video_i386_vbeblit_R8G8B8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGB888_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -393,7 +519,7 @@ } void -grub_video_i386_vbeblit_index_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_index_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) === modified file 'video/i386/pc/vbefill.c' --- video/i386/pc/vbefill.c 2007-07-21 22:32:33 +0000 +++ video/i386/pc/vbefill.c 2008-07-03 13:49:18 +0000 @@ -34,51 +34,63 @@ #include void -grub_video_i386_vbefill_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbefill_direct32 (struct grub_video_i386_vbeblit_info *dst, grub_video_color_t color, int x, int y, int width, int height) { int i; int j; grub_uint32_t *dstptr; - - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ + grub_size_t rowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + rowskip = dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + /* Get the start address. */ + dstptr = (grub_uint32_t *) grub_video_vbe_get_video_ptr (dst, x, y); for (j = 0; j < height; j++) { - dstptr = (grub_uint32_t *)grub_video_vbe_get_video_ptr (dst, x, y + j); - for (i = 0; i < width; i++) *dstptr++ = color; + + /* Advance the dest pointer to the right location on the next line. */ + dstptr = (grub_uint32_t *) (((char *) dstptr) + rowskip); } } void -grub_video_i386_vbefill_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - grub_video_color_t color, int x, int y, - int width, int height) +grub_video_i386_vbefill_direct24 (struct grub_video_i386_vbeblit_info *dst, + grub_video_color_t color, int x, int y, + int width, int height) { int i; int j; + grub_size_t rowskip; grub_uint8_t *dstptr; - grub_uint8_t fillr = (grub_uint8_t)((color >> 0) & 0xFF); - grub_uint8_t fillg = (grub_uint8_t)((color >> 8) & 0xFF); - grub_uint8_t fillb = (grub_uint8_t)((color >> 16) & 0xFF); + grub_uint8_t fill0 = (grub_uint8_t)((color >> 0) & 0xFF); + grub_uint8_t fill1 = (grub_uint8_t)((color >> 8) & 0xFF); + grub_uint8_t fill2 = (grub_uint8_t)((color >> 16) & 0xFF); - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + rowskip = dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + /* Get the start address. */ + dstptr = (grub_uint8_t *) grub_video_vbe_get_video_ptr (dst, x, y); for (j = 0; j < height; j++) { - dstptr = (grub_uint8_t *)grub_video_vbe_get_video_ptr (dst, x, y + j); - for (i = 0; i < width; i++) { - *dstptr++ = fillr; - *dstptr++ = fillg; - *dstptr++ = fillb; + *dstptr++ = fill0; + *dstptr++ = fill1; + *dstptr++ = fill2; } + + /* Advance the dest pointer to the right location on the next line. */ + dstptr += rowskip; } } @@ -89,18 +101,24 @@ { int i; int j; + grub_size_t rowskip; grub_uint8_t *dstptr; grub_uint8_t fill = (grub_uint8_t)color & 0xFF; - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + rowskip = dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + /* Get the start address. */ + dstptr = (grub_uint8_t *) grub_video_vbe_get_video_ptr (dst, x, y); for (j = 0; j < height; j++) { - dstptr = (grub_uint8_t *)grub_video_vbe_get_video_ptr (dst, x, y + j); - for (i = 0; i < width; i++) *dstptr++ = fill; + + /* Advance the dest pointer to the right location on the next line. */ + dstptr += rowskip; } } @@ -112,9 +130,6 @@ int i; int j; - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ - for (j = 0; j < height; j++) for (i = 0; i < width; i++) set_pixel (dst, x+i, y+j, color); === modified file 'video/i386/pc/vbeutil.c' --- video/i386/pc/vbeutil.c 2007-07-21 22:32:33 +0000 +++ video/i386/pc/vbeutil.c 2008-07-03 14:10:25 +0000 @@ -52,6 +52,11 @@ + y * source->mode_info->pitch + x; break; + + /* case 1: */ + /* For 1-bit bitmaps, addressing needs to be done at the bit level + * and it doesn't make sense, in general, to ask for a pointer + * to a particular pixel's data. */ } return ptr; @@ -86,6 +91,17 @@ color = *(grub_uint8_t *)get_data_ptr (source, x, y); break; + case 1: + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED) + { + int bit_index = y * source->mode_info->width + x; + grub_uint8_t *ptr = (grub_uint8_t *)source->data + + bit_index / 8; + int bit_pos = 7 - bit_index % 8; + color = (*ptr >> bit_pos) & 0x01; + } + break; + default: break; } @@ -143,6 +159,17 @@ } break; + case 1: + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED) + { + int bit_index = y * source->mode_info->width + x; + grub_uint8_t *ptr = (grub_uint8_t *)source->data + + bit_index / 8; + int bit_pos = 7 - bit_index % 8; + *ptr = (*ptr & ~(1 << bit_pos)) | ((color & 0x01) << bit_pos); + } + break; + default: break; } === modified file 'video/readers/jpeg.c' --- video/readers/jpeg.c 2008-01-22 13:50:38 +0000 +++ video/readers/jpeg.c 2008-07-03 13:49:18 +0000 @@ -542,7 +542,7 @@ if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8)) + GRUB_VIDEO_BLIT_FORMAT_RGB_888)) return grub_errno; data->bit_mask = 0x0; === modified file 'video/readers/png.c' --- video/readers/png.c 2008-03-31 11:00:48 +0000 +++ video/readers/png.c 2008-07-03 13:49:18 +0000 @@ -231,7 +231,7 @@ { if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8)) + GRUB_VIDEO_BLIT_FORMAT_RGB_888)) return grub_errno; data->bpp = 3; } @@ -239,7 +239,7 @@ { if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8)) + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888)) return grub_errno; data->bpp = 4; } === modified file 'video/readers/tga.c' --- video/readers/tga.c 2007-12-30 08:52:06 +0000 +++ video/readers/tga.c 2008-07-03 13:49:18 +0000 @@ -397,7 +397,7 @@ { grub_video_bitmap_create (bitmap, header.image_width, header.image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8); + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888); if (grub_errno != GRUB_ERR_NONE) { grub_file_close (file); @@ -420,7 +420,7 @@ { grub_video_bitmap_create (bitmap, header.image_width, header.image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8); + GRUB_VIDEO_BLIT_FORMAT_RGB_888); if (grub_errno != GRUB_ERR_NONE) { grub_file_close (file); === modified file 'video/video.c' --- video/video.c 2008-01-01 12:02:07 +0000 +++ video/video.c 2008-07-03 14:12:08 +0000 @@ -152,15 +152,22 @@ if (mode_info->bpp == 32) { if ((mode_info->red_mask_size == 8) + && (mode_info->red_field_pos == 16) + && (mode_info->green_mask_size == 8) + && (mode_info->green_field_pos == 8) + && (mode_info->blue_mask_size == 8) + && (mode_info->blue_field_pos == 0)) + { + return GRUB_VIDEO_BLIT_FORMAT_BGRA_8888; + } + if ((mode_info->red_mask_size == 8) && (mode_info->red_field_pos == 0) && (mode_info->green_mask_size == 8) && (mode_info->green_field_pos == 8) && (mode_info->blue_mask_size == 8) - && (mode_info->blue_field_pos == 16) - && (mode_info->reserved_mask_size == 8) - && (mode_info->reserved_field_pos == 24)) + && (mode_info->blue_field_pos == 16)) { - return GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8; + return GRUB_VIDEO_BLIT_FORMAT_RGBA_8888; } } @@ -174,7 +181,7 @@ && (mode_info->blue_mask_size == 8) && (mode_info->blue_field_pos == 16)) { - return GRUB_VIDEO_BLIT_FORMAT_R8G8B8; + return GRUB_VIDEO_BLIT_FORMAT_RGB_888; } } @@ -305,6 +312,19 @@ return grub_video_adapter_active->blit_glyph (glyph, color, x, y); } +/* Draw string to screen using specified color and font. */ +grub_err_t +grub_video_draw_string (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y) +{ + if (! grub_video_adapter_active) + return grub_error (GRUB_ERR_BAD_DEVICE, "No video mode activated"); + + return grub_video_adapter_active->draw_string (str, font, color, + left_x, baseline_y); +} + /* Blit bitmap to screen. */ grub_err_t grub_video_blit_bitmap (struct grub_video_bitmap *bitmap, # Begin bundle IyBCYXphYXIgcmV2aXNpb24gYnVuZGxlIHY0CiMKQlpoOTFBWSZTWQq9+dcBJP7/gH////////// //////////9hSbz6Z5ii9RRFx6z56752guz14dj0d7O7Pm+fVvaBpu777rvvW+H2Gl0AG9d3uT7a o0ZKjrvm+l7dT3vbnqvY+STj5U3AD6b08+4GntqBdgaACutvNew9buOe3cXt9Hc2h2z60+gD5ByD vsBe92971e72N6wH3vfO9q2+j4h5OtVe++3nrvsBux4vT6Huw7aCoCn3vuejrc6+3J7WW2fc0pNz J6PvJ1i6rcx2tztvgXbveNrvfea64AL7urlZqUCSXwB9V6nHp3VTABFHpXN7sT3W2Kl92acF9309 e5W7s9Do3fOLRHnh6e73jDve6vs21jXuzvLRXrvvB73tr763Pj2YL17dwaFDZqabnA6vppVA6B99 5fXiu23o329cBaw22iltSBOjdsSqJGQBvRvoMQVQInvu+j3u452wJUaUe4Ndh9dV9ADT1gg0+7LY fXQqXrBCkikmzHmvR1Evqqt5zX3Z1qVPneb6Gg6AAeh6PT2QejdgrOwGnkzll3c9dfT68tWzEA9O h9ar2wB7YOhPe5trvd7tew7remvTrdj6z7ezTbfdaoLoWLcO3d2Vzz7azUOp7e5bd2BYVdO+sb3I b32e+SeHg1b6+9vr5b58ebhrFb33nauFDesmzT3z3rd3OsjOe73YXeDyD274bFu994vtNhd7PQND vvbfbuXvcD7sZ2AG8AaBL5x6IvsU83F8O6+++5j3JUL2a06N77G8efcc12DWq1q76Lzap1117r2L 7tjO1udsPvOe8x2rjbttypQXdxxO7AyOLqqOzG20a4GdOhSp3r775w+65A02sqkkGyxuOt1YznXY DtbtpTdndN10doYrFwBA22tyttnLVbYjVjoKoyOHd27vXgOqbYbYIbe7Ts1UK5m6BRGs1ozoO2KH bu1udYmN3K2kKJe+IvvsDu2632nwetVbJbYjZfOnatruW+hPt91tr3zeXe3A3Uzb33m9vbWq5tpn dtGnDVbZ0K0u22ud10tpKGmR0DbCzGNh75rb72Dn1fdyfVPmloSRAJoAgQJoAE0BNCYE0ajJqPE1 GjI00Bk9QPSNNqephGgSmgQQQghEym0npU9oamKepoabSPSPU9QAAADR6gAAADQEgkRBE0aTJpqZ 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 F3JFOFCQCr351w==