# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080803040022-bxhp6libdkx40muv # target_branch: ../../repo/trunk-clean # testament_sha1: a9261a62ae1b371e0a9b27a0f1f55c4c880aaf1e # timestamp: 2008-08-03 07:26:43 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080803034658-kcm160bcz0e1rt9f # # Begin patch === modified file '.bzrignore' --- .bzrignore 2008-07-19 21:43:42 +0000 +++ .bzrignore 2008-07-19 21:44:04 +0000 @@ -7,3 +7,5 @@ ./conf/sparc64-ieee1275.mk ./conf/i386-coreboot.mk ./build* +./util/fonttool/build +./util/fonttool/fonttool.iws === modified file 'ChangeLog' --- ChangeLog 2008-08-02 22:24:34 +0000 +++ ChangeLog 2008-08-03 03:57:46 +0000 @@ -64,7 +64,6 @@ (memdisk_mod_SOURCES, memdisk_mod_CFLAGS) (memdisk_mod_LDFLAGS): New variables. * conf/i386-coreboot.rmk: Likewise. - * conf/i386-ieee1275.rmk: Likewise. 2008-08-02 Robert Millan @@ -197,6 +196,72 @@ * normal/main.c (get_line): Fix buffer overflow bug. +2008-07-28 Colin D Bennett + + High resolution timer support. Implemented for x86 CPUs using TSC. + Extracted generic grub_millisleep() so it's linked in only as needed. + This requires a Pentium compatible CPU; if the RDTSC instruction is + not supported, then it falls back on the generic grub_get_time_ms() + implementation that uses the machine's RTC. + + * conf/i386-efi.rmk: Added new source files to kernel_elf_SOURCES. + + * conf/i386-pc.rmk: Likewise. + + * conf/sparc64-ieee1275.rmk: Likewise. + + * conf/powerpc-ieee1275.rmk: Likewise. + + + * kern/generic/rtc_get_time_ms.c: New file. + + * kern/generic/millisleep.c: New file. + + * kern/misc.c (grub_millisleep_generic): Removed. + + * 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. New inline + function. + (grub_cpu_is_cpuid_supported): New inline function. + (grub_cpu_is_tsc_supported): New inline function. + (grub_tsc_init): New function prototype. + (grub_tsc_get_time_ms): New function prototype. + + * kern/i386/tsc.c (grub_get_time_ms): New file. New function. + (calibrate_tsc): New static function. + (grub_tsc_init): New function. + + * include/grub/time.h (grub_millisleep_generic): Removed. + (grub_get_time_ms): New function. + (grub_install_get_time_ms): New function. + + * kern/time.c (grub_get_time_ms): New function. + (grub_install_get_time_ms): New function. + + * kern/i386/efi/init.c (grub_millisleep): Removed. + (grub_machine_init): Call grub_tsc_init. + + * kern/i386/linuxbios/init.c (grub_machine_init): Install the RTC + get_time_ms() implementation. + + * kern/sparc64/ieee1275/init.c (grub_millisleep): Removed. + (ieee1275_get_time_ms): New function. + (grub_machine_init): Install get_time_ms() implementation. + + * kern/i386/pc/init.c (grub_machine_init): Call grub_tsc_init(). + (grub_millisleep): Removed. + + * kern/ieee1275/init.c (grub_millisleep): Removed. + (grub_machine_init): Install ieee1275_get_time_ms() implementation. + (ieee1275_get_time_ms): New static function. + (grub_get_rtc): Now calls ieee1275_get_time_ms(), which does the real + work. + + * conf/i386-ieee1275.rmk: Likewise. + 2008-07-28 Robert Millan * partmap/apple.c (GRUB_APPLE_HEADER_MAGIC): New macro. === modified file 'Makefile.in' --- Makefile.in 2008-07-24 13:56:30 +0000 +++ Makefile.in 2008-07-28 02:00:30 +0000 @@ -89,6 +89,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@ @@ -136,13 +169,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-09 14:39:39 +0000 @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -48,12 +49,22 @@ grub_err_t err; char *modevar; - grub_printf ("List of compatible video modes:\n"); - err = grub_vbe_probe (&controller_info); if (err != GRUB_ERR_NONE) return err; + int row = 0; + + grub_printf ("VBE info: version: %d.%d OEM software rev: %d.%d\n", + controller_info.version >> 8, + controller_info.version & 0xFF, + controller_info.oem_software_rev >> 8, + controller_info.oem_software_rev & 0xFF); + row++; + grub_printf (" total memory: %d KiB\n", + (controller_info.total_memory << 16) / 1024); + row++; + /* Because the information on video modes is stored in a temporary place, it is better to copy it to somewhere safe. */ p = video_mode_list = real2pm (controller_info.video_mode_ptr); @@ -67,6 +78,10 @@ grub_memcpy (saved_video_mode_list, video_mode_list, video_mode_list_size); + grub_printf ("List of compatible video modes:\n"); + grub_printf ("Legend: P=Packed pixel, D=Direct color, " + "mask/pos=R/G/B/reserved\n"); + /* Walk through all video modes listed. */ for (p = saved_video_mode_list; *p != 0xFFFF; p++) { @@ -103,10 +118,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 +131,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-19 18:25:18 +0000 @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -26,15 +27,38 @@ #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; +}; + + +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 +68,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 +90,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 +109,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 +133,1076 @@ 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 (0), 0}, + {640, 480, MODE_TYPE_RGB (0), 0}, + {1024, 768, MODE_TYPE_RGB (0), 0}, + {1024, 768, MODE_TYPE_RGB (0), 1}, +}; + +#define NUM_BENCHMARK_CONFIGS (sizeof(benchmark_configs) \ + / sizeof(benchmark_configs[0])) + +struct benchmark_result +{ + /* If set to 1, the test was able to run successfully; + * 0 means there was an error. */ + int test_passed; + + /* Bits per pixel for the video mode used. */ + int bpp; + + /* All fps are in fps * 10 to achieve one decimal place. */ + /* Set to 0 to indicate the test could not be run. */ + grub_int32_t fill_fps; + grub_int32_t blit_fps; + grub_int32_t blend_fps; +}; + +#define BENCHMARK_RESULT_FPS_SCALE 10 + +static void +move_rectangle_one_step (int *x, int *y, + int width, int height, + int *vx, int *vy, const grub_video_rect_t * bounds) +{ + int newx = *x + *vx; + int newy = *y + *vy; + + /* Check collision w/ left */ + if (newx < bounds->x && *vx < 0) + { + *vx = -*vx; + newx = *x + *vx; + } + /* Check collision w/ right */ + if (newx + width > bounds->x + bounds->width && *vx > 0) + { + *vx = -*vx; + newx = *x + *vx; + } + /* Check collision w/ top */ + if (newy < 0 && *vy < 0) + { + *vy = -*vy; + newy = *y + *vy; + } + /* Check collision w/ bottom */ + if (newy + height > bounds->y + bounds->height && *vy > 0) + { + *vy = -*vy; + newy = *y + *vy; + } + + *x = newx; + *y = newy; +} + +/** + * Run the benchmark test for a particular video mode, which is specified + * by ``*config``. The results of the test are stored in ``*result``. + */ +static void +do_benchmark (const struct benchmark_config *config, + struct benchmark_result *result, + struct videotest_options *vt_opts) +{ + struct grub_video_mode_info modeinfo; + + result->test_passed = 0; + result->fill_fps = 0; + result->blit_fps = 0; + result->blend_fps = 0; + result->bpp = 0; + + int mode_type = config->mode_type; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (config->width, config->height, + mode_type) != GRUB_ERR_NONE) + return; + + if (grub_video_get_info (&modeinfo) == GRUB_ERR_NONE) + result->bpp = modeinfo.bpp; + + /* Full screen bitmap blit test. */ + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + + /* For measuring timing. */ + grub_uint32_t frame_count; + grub_uint64_t start_time; + grub_uint64_t desired_stop_time; + grub_uint32_t duration; + + + /*** FILL TEST ***/ + + /* Alternates between 0 and 1 to change the color. */ + int color_flag = 0; + grub_video_color_t fillcolors[2]; + fillcolors[0] = grub_video_map_rgb (0, 0, 255); + fillcolors[1] = grub_video_map_rgb (255, 255, 0); + + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + grub_video_fill_rect (fillcolors[color_flag], + view.x, view.y, view.width, view.height); + grub_video_swap_buffers (); + color_flag ^= 1; + frame_count++; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was one. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->fill_fps = 0; + } + else + { + result->fill_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + + /*** BLIT TEST ***/ + + /* Generate two bitmaps, the same size as the screen. */ + const int bitmapwidth = view.width, bitmapheight = view.height; + struct grub_video_bitmap *bitmap1; + struct grub_video_bitmap *bitmap2; + enum grub_video_blit_format bitmap_format = config->use_rgba_bitmaps + ? GRUB_VIDEO_BLIT_FORMAT_RGBA_8888 : GRUB_VIDEO_BLIT_FORMAT_RGB_888; + + if (grub_video_bitmap_create (&bitmap1, bitmapwidth, bitmapheight, + bitmap_format) != GRUB_ERR_NONE) + return; + + if (grub_video_bitmap_create (&bitmap2, bitmapwidth, bitmapheight, + bitmap_format) != GRUB_ERR_NONE) + { + grub_video_bitmap_destroy (bitmap1); + return; + } + + int offset; + int x; + int y; + grub_uint8_t *data; + + offset = 0; + data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bitmapheight; y++) + { + for (x = 0; x < bitmapwidth; x++) + { + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + if (config->use_rgba_bitmaps) + data[offset++] = 255; + } + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap2); + for (y = 0; y < bitmapheight; y++) + { + for (x = 0; x < bitmapwidth; x++) + { + data[offset++] = x + y; /* red */ + data[offset++] = x * x + y * y; /* green */ + data[offset++] = x * x / 4 + y * y / 4; /* blue */ + if (config->use_rgba_bitmaps) + data[offset++] = 255; + } + } + + + /* Now do the blit test, alternating between the two bitmaps. */ + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + int cur = 0; /* Which bitmap to draw this frame. */ + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + struct grub_video_bitmap *current_bitmap = cur == 0 ? bitmap1 : bitmap2; + grub_video_blit_bitmap (current_bitmap, + GRUB_VIDEO_BLIT_REPLACE, + view.x, view.y, 0, 0, bitmapwidth, + bitmapheight); + grub_video_swap_buffers (); + frame_count++; + cur ^= 1; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was once. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->blit_fps = 0; + } + else + { + result->blit_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + grub_video_bitmap_destroy (bitmap2); + grub_video_bitmap_destroy (bitmap1); + + + + /*** BLEND TEST ***/ + + /* Generate two bitmaps, with alpha translucency. */ + const int bbw = view.width * 2 / 3; + const int bbh = view.height * 2 / 3; + + if (grub_video_bitmap_create (&bitmap1, bbw, bbh, + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) != + GRUB_ERR_NONE) + return; + + if (grub_video_bitmap_create (&bitmap2, bbw, bbh, + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) != + GRUB_ERR_NONE) + { + grub_video_bitmap_destroy (bitmap1); + return; + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bbh; y++) + { + for (x = 0; x < bbw; x++) + { + /* Calculate a to be increasing away from the center. */ + int dx = 256 * (x - bbw / 2) / (bbw / 2); + int dy = 256 * (y - bbh / 2) / (bbh / 2); + int a = dx * dx + dy * dy; + /* range for a = 0 .. 2*(256^2) = 2*2^16 = 2^17 */ + a >>= 17 - 8; /* Make range 0..256. */ + if (a > 255) + a = 255; + + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + data[offset++] = 255 - a; + } + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap2); + for (y = 0; y < bbh; y++) + { + for (x = 0; x < bbw; x++) + { + data[offset++] = x + y; /* red */ + data[offset++] = x * x + y * y; /* green */ + data[offset++] = x * x / 4 + y * y / 4; /* blue */ + data[offset++] = 255; + } + } + + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + + grub_video_color_t bgcolor = grub_video_map_rgb (80, 80, 80); + + /* Bitmap locations. */ + int b1x = 0; + int b1y = 0; + int b2x = view.width - bbw; + int b2y = 0; + /* Bitmap velocities. */ + int b1vx = 8; + int b1vy = 12; + int b2vx = -10; + int b2vy = 9; + + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + move_rectangle_one_step (&b1x, &b1y, bbw, bbh, &b1vx, &b1vy, &view); + move_rectangle_one_step (&b2x, &b2y, bbw, bbh, &b2vx, &b2vy, &view); + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height); + grub_video_blit_bitmap (bitmap2, + GRUB_VIDEO_BLIT_BLEND, + b2x, b2y, 0, 0, bbw, bbh); + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_BLEND, + b1x, b1y, 0, 0, bbw, bbh); + grub_video_swap_buffers (); + frame_count++; + cur ^= 1; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was once. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->blend_fps = 0; + } + else + { + result->blend_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + grub_video_bitmap_destroy (bitmap2); + grub_video_bitmap_destroy (bitmap1); + + grub_video_restore (); + result->test_passed = 1; +} + +/** + * Run a benchmark test in a series of video modes. + * The results are reported in tabular form. This will be helpful to + * determine how effective various optimizations are. + */ +static void +benchmark_test (struct videotest_options *vt_opts) +{ + unsigned int i; + struct benchmark_result results[NUM_BENCHMARK_CONFIGS]; + + /* Set option default values. */ + if (vt_opts->test_time == 0) + vt_opts->test_time = DEFAULT_TEST_TIME; + + /* Run benchmarks. */ + for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++) + { + grub_error_push (); + do_benchmark (&benchmark_configs[i], &results[i], vt_opts); + } + + grub_print_error (); + + /* Display results. */ + grub_printf ("Benchmark results (in frames/s):\n"); + grub_printf ("(W=Width, H=Height, B=Bits per pixel,\n" + " T=Mode Type, A=Bitmap Alpha)\n"); + grub_printf (" W H B T A FILL BLIT BLEND\n"); + for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++) + { + struct benchmark_config *c = &benchmark_configs[i]; + struct benchmark_result *r = &results[i]; + + if (r->test_passed) + { + grub_printf ("%4dx%4d %2d %d %d: %4d.%d %4d.%d %4d.%d\n", + c->width, c->height, r->bpp, + c->mode_type & 0x0F, + c->use_rgba_bitmaps, + r->fill_fps / BENCHMARK_RESULT_FPS_SCALE, + r->fill_fps % BENCHMARK_RESULT_FPS_SCALE, + r->blit_fps / BENCHMARK_RESULT_FPS_SCALE, + r->blit_fps % BENCHMARK_RESULT_FPS_SCALE, + r->blend_fps / BENCHMARK_RESULT_FPS_SCALE, + r->blend_fps % BENCHMARK_RESULT_FPS_SCALE); + } + else + { + grub_printf ("%4dx%4d %2d %d Not supported.\n", + c->width, c->height, + ((c->mode_type & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK) + >> GRUB_VIDEO_MODE_TYPE_DEPTH_POS), + c->mode_type & 0x0F); + } + } + + grub_errno = GRUB_ERR_NONE; +} + + +/** + * Test time functions. + */ +static void +clock_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + /* Draw a progress bar that animates in sync with time. */ + + grub_video_rect_t bar_frame; + bar_frame.width = view.width - 2 * view.width / 10; + bar_frame.height = view.height / 20; + bar_frame.x = view.x + view.width / 10; + bar_frame.y = view.y + view.height - bar_frame.height - view.height / 10; + + grub_video_color_t bgcolor = grub_video_map_rgb (50, 50, 50); + grub_video_color_t framecolor = grub_video_map_rgb (255, 255, 255); + grub_video_color_t barbgcolor = grub_video_map_rgb (0, 0, 128); + grub_video_color_t barcolor = grub_video_map_rgb (100, 100, 255); + + grub_uint32_t frame_count; + grub_uint64_t start_time; + grub_uint64_t barstart; + grub_uint32_t barlength = 1000; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + frame_count = 0; + start_time = grub_get_time_ms (); + barstart = grub_get_time_ms (); + + while (grub_checkkey () == -1) + { + grub_uint64_t now; + grub_uint32_t bartime; + now = grub_get_time_ms (); + /* If the time limit option is set, then check if it's exceeded. */ + if (vt_opts->test_time != 0 + && now >= start_time + vt_opts->test_time * 1000) + break; + bartime = now - barstart; + if (bartime > barlength) + { + barstart = grub_get_time_ms (); /* Start over. */ + bartime = barlength; + } + + /* Clear screen. */ + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height); + + /* Border. */ + grub_video_fill_rect (framecolor, + bar_frame.x - 1, bar_frame.y - 1, + bar_frame.width + 2, bar_frame.height + 2); + + /* Bar background. */ + int barwidth = bar_frame.width * bartime / barlength; + grub_video_fill_rect (barbgcolor, bar_frame.x + barwidth, + bar_frame.y, bar_frame.width - barwidth, + bar_frame.height); + /* Bar foreground. */ + grub_video_fill_rect (barcolor, bar_frame.x, bar_frame.y, + barwidth, bar_frame.height); + grub_video_swap_buffers (); + frame_count++; + } + + grub_uint32_t duration = grub_get_time_ms () - start_time; + grub_uint32_t fps_x10 = 10 * 1000 * frame_count / duration; + + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was one. */ + grub_video_restore (); + grub_printf ("Average frame rate: %d.%d fps\n", fps_x10 / 10, fps_x10 % 10); +} + + +/** + * Test double buffering. + */ +static void +doublebuf_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + grub_video_color_t red = grub_video_map_rgb (255, 50, 50); + grub_video_color_t yellow = grub_video_map_rgb (255, 255, 0); + grub_video_color_t green = grub_video_map_rgb (20, 255, 20); + grub_video_color_t blue = grub_video_map_rgb (50, 50, 255); + grub_video_color_t black = grub_video_map_rgb (0, 0, 0); + grub_video_color_t bgcolor = grub_video_map_rgb (255, 255, 255); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (red, 100, 100, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (yellow, 120, 120, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (green, 140, 140, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (blue, 160, 160, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (black, 180, 180, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_restore (); +} + + +/** + * Test text rendering. + */ +static void +text_test (struct videotest_options *vt_opts) +{ + grub_video_color_t color; + const char *s; + int view_x; + int view_y; + int view_width; + int view_height; + int xpos; + int ypos; + int i; + grub_font_t font1; + grub_font_t font2; + grub_font_t font3; + + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_get_viewport ((unsigned int *) &view_x, + (unsigned int *) &view_y, + (unsigned int *) &view_width, + (unsigned int *) &view_height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + if (!(font1 = grub_font_get ("New Century Schoolbook 24")) + || !(font2 = grub_font_get ("Helvetica Bold 14")) + || !(font3 = grub_font_get ("Helvetica 10"))) + { + grub_video_restore (); + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + color = grub_video_map_rgb (0, 0, 0); + grub_video_fill_rect (color, 0, 0, view_width, view_height); + + color = grub_video_map_rgb (255, 0, 0); + grub_video_fill_rect (color, 0, 0, 100, 100); + + color = grub_video_map_rgb (0, 255, 255); + grub_video_fill_rect (color, 100, 100, 100, 100); + + color = grub_video_map_rgb (255, 255, 255); + + xpos = 10; + ypos = 30; + s = "Hello, World!"; + for (i = 0; i < 40; i++) + { + if (xpos + grub_font_get_string_width (font1, s) >= view_width) + { + /* The string will wrap; go to the beginning of the next line. */ + xpos = 10; + ypos += (grub_font_get_descent (font1) + + grub_font_get_ascent (font1)); + } + grub_video_draw_string ("Hello, World!", + font1, grub_video_map_rgb (255, 255, 0), + view_x + xpos, view_y + ypos); + + xpos += grub_font_get_string_width (font1, s); + } + + xpos = 300; + ypos = 450; + grub_video_draw_string (grub_font_get_name (font1), + font1, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + ypos += grub_font_get_descent (font1) + grub_font_get_ascent (font2) + 2; + grub_video_draw_string (grub_font_get_name (font2), + font2, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + ypos += grub_font_get_descent (font2) + grub_font_get_ascent (font3) + 2; + grub_video_draw_string (grub_font_get_name (font3), + font3, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + + grub_video_swap_buffers (); + grub_getkey (); + grub_video_restore (); + grub_errno = GRUB_ERR_NONE; +} + + +/** + * Test bitmap scaling. + */ +static void +scale_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_errno = GRUB_ERR_NONE; + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + grub_font_t font; + if (!(font = grub_font_get ("Helvetica 10"))) + { + grub_video_restore (); + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + grub_video_color_t color; + + int text_y = 0; + int text_height = 25; + + color = grub_video_map_rgb (44, 44, 200); + grub_video_fill_rect (color, view.x, view.y, view.width, view.height); + color = grub_video_map_rgb (255, 255, 255); + + grub_video_draw_string ("Loading image", + font, color, 10, text_y += text_height); + + enum grub_video_bitmap_scale_method scale_method = + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST; + const char *bitmap_name = "/boot/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-08-01 03:06:55 +0000 +++ conf/common.rmk 2008-08-03 04:00:22 +0000 @@ -274,6 +274,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 \ + gfxmenu.mod \ read.mod sleep.mod loadenv.mod crc.mod # For hello.mod. @@ -281,6 +282,16 @@ hello_mod_CFLAGS = $(COMMON_CFLAGS) hello_mod_LDFLAGS = $(COMMON_LDFLAGS) +# For gfxmenu.mod. +gfxmenu_mod_SOURCES = \ + gfxmenu/gfxmenu.c \ + gfxmenu/model.c \ + gfxmenu/view.c \ + gfxmenu/widget-box.c \ + gfxmenu/stringutil.c +gfxmenu_mod_CFLAGS = $(COMMON_CFLAGS) +gfxmenu_mod_LDFLAGS = $(COMMON_LDFLAGS) + # For boot.mod. boot_mod_SOURCES = commands/boot.c boot_mod_CFLAGS = $(COMMON_CFLAGS) @@ -317,7 +328,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) === modified file 'conf/i386-coreboot.rmk' --- conf/i386-coreboot.rmk 2008-08-02 11:17:44 +0000 +++ conf/i386-coreboot.rmk 2008-08-03 03:57:46 +0000 @@ -16,6 +16,7 @@ kern/main.c kern/device.c \ 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/time.c \ kern/i386/dl.c kern/parser.c kern/partition.c \ kern/env.c \ term/i386/pc/console.c \ === modified file 'conf/i386-efi.rmk' --- conf/i386-efi.rmk 2008-07-27 12:51:30 +0000 +++ conf/i386-efi.rmk 2008-07-28 16:26:46 +0000 @@ -84,7 +84,10 @@ 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/rtc_get_time_ms.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-ieee1275.rmk' --- conf/i386-ieee1275.rmk 2008-08-02 11:17:44 +0000 +++ conf/i386-ieee1275.rmk 2008-08-03 03:57:46 +0000 @@ -19,6 +19,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/millisleep.c \ kern/ieee1275/ieee1275.c \ term/ieee1275/ofconsole.c term/i386/pc/at_keyboard.c \ disk/ieee1275/ofdisk.c \ === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-07-27 12:51:30 +0000 +++ conf/i386-pc.rmk 2008-07-28 17:02:56 +0000 @@ -42,13 +42,19 @@ kernel_img_SOURCES = kern/i386/pc/startup.S kern/main.c kern/device.c \ 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/time.c \ kern/i386/dl.c kern/i386/pc/init.c kern/parser.c kern/partition.c \ + kern/i386/tsc.c \ + kern/generic/rtc_get_time_ms.c \ + kern/generic/millisleep.c \ kern/env.c \ + kern/menu_viewer.c \ term/i386/pc/console.c \ symlist.c kernel_img_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 \ + menu_viewer.h \ machine/biosdisk.h machine/boot.h machine/console.h machine/init.h \ machine/memory.h machine/loader.h machine/vga.h machine/vbe.h machine/kernel.h kernel_img_CFLAGS = $(COMMON_CFLAGS) @@ -246,7 +252,7 @@ play_mod_LDFLAGS = $(COMMON_LDFLAGS) # For video.mod. -video_mod_SOURCES = video/video.c +video_mod_SOURCES = video/video.c video/setmode.c video_mod_CFLAGS = $(COMMON_CFLAGS) video_mod_LDFLAGS = $(COMMON_LDFLAGS) @@ -261,7 +267,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-08-02 11:17:44 +0000 +++ conf/powerpc-ieee1275.rmk 2008-08-03 03:57:46 +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) === modified file 'conf/x86_64-efi.rmk' --- conf/x86_64-efi.rmk 2008-07-27 12:51:30 +0000 +++ conf/x86_64-efi.rmk 2008-07-28 16:23:46 +0000 @@ -87,6 +87,7 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/x86_64/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 \ + kern/generic/millisleep.c kern/generic/rtc_get_time_ms.c \ term/efi/console.c disk/efi/efidisk.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 \ === modified file 'config.h.in' --- config.h.in 2008-07-13 00:55:15 +0000 +++ config.h.in 2008-07-19 21:46:40 +0000 @@ -113,10 +113,37 @@ /* Number of bits in a file offset, on hosts where this is settable. */ #undef _FILE_OFFSET_BITS +/* Define for large files, on AIX-style hosts. */ +#undef _LARGE_FILES + +/* Define to 1 if on MINIX. */ +#undef _MINIX + +/* Define to 2 if the system does not provide POSIX.1 features except with + this defined. */ +#undef _POSIX_1_SOURCE + +/* Define to 1 if you need to in order for `stat' and other things to work. */ +#undef _POSIX_SOURCE + +/* Enable extensions on AIX 3, Interix. */ +#ifndef _ALL_SOURCE +# undef _ALL_SOURCE +#endif /* Enable GNU extensions on systems that have them. */ #ifndef _GNU_SOURCE # undef _GNU_SOURCE #endif +/* Enable threading extensions on Solaris. */ +#ifndef _POSIX_PTHREAD_SEMANTICS +# undef _POSIX_PTHREAD_SEMANTICS +#endif +/* Enable extensions on HP NonStop. */ +#ifndef _TANDEM_SOURCE +# undef _TANDEM_SOURCE +#endif +/* Enable general extensions on Solaris. */ +#ifndef __EXTENSIONS__ +# undef __EXTENSIONS__ +#endif -/* Define for large files, on AIX-style hosts. */ -#undef _LARGE_FILES === 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-29 14:51:29 +0000 @@ -0,0 +1,689 @@ +/* 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 + +/* 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_buffile_open (filename, 1024); + 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 + grub_off_t section_end = grub_file_tell (file) + section.length; + if ((int) grub_file_seek (file, section_end) == -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-08-01 03:06:55 +0000 +++ font/manager.c 1970-01-01 00:00:00 +0000 @@ -1,283 +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 -#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_buffile_open (filename, 0); - 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-28 21:35:40 +0000 +++ genmk.rb 2008-08-03 03:57:46 +0000 @@ -126,7 +126,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} @@ -158,18 +158,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 directory 'gfxmenu' === added file 'gfxmenu/gfxmenu.c' --- gfxmenu/gfxmenu.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gfxmenu.c 2008-07-27 00:06:20 +0000 @@ -0,0 +1,221 @@ +/* gfxmenu.c - Graphical menu interface controller. */ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void switch_to_text_menu (void) +{ + grub_env_set ("menuviewer", "terminal"); +} + +static void +process_key_press (int c, + grub_gfxmenu_model_t model, + grub_gfxmenu_view_t view, + int nested, + int *should_exit) +{ + /* When a key is pressed, stop the timeout. */ + grub_gfxmenu_model_clear_timeout (model); + + if (c == 'j' || c == GRUB_TERM_DOWN) + { + int i = grub_gfxmenu_model_get_selected_index (model); + int num_items = grub_gfxmenu_model_get_num_entries (model); + if (i < num_items - 1) + { + i++; + grub_gfxmenu_model_set_selected_index (model, i); + } + } + else if (c == 'k' || c == GRUB_TERM_UP) + { + int i = grub_gfxmenu_model_get_selected_index (model); + if (i > 0) + { + i--; + grub_gfxmenu_model_set_selected_index (model, i); + } + } + else if (c == '\r' || c == '\n' || c == GRUB_TERM_RIGHT) + { + int selected = grub_gfxmenu_model_get_selected_index (model); + int num_entries = grub_gfxmenu_model_get_num_entries (model); + if (selected >= 0 && selected < num_entries) + { + grub_menu_entry_t entry = + grub_gfxmenu_model_get_entry (model, selected); + grub_gfxmenu_view_execute_entry (view, entry); + } + } + else if (c == 'c') + { + grub_gfxmenu_view_run_terminal (view); + if (grub_errno != GRUB_ERR_NONE) + *should_exit = 1; + } + else if (c == 't') + { + /* The write hook for 'menuviewer' will cause + * grub_menu_viewer_should_return to return nonzero. */ + switch_to_text_menu (); + *should_exit = 1; + } + else if (c == '1') + { + grub_gfxmenu_view_load_theme (view, + "/boot/grub/themes/proto/theme.txt"); + } + else if (c == '2') + { + grub_gfxmenu_view_load_theme (view, + "/boot/grub/themes/winter/theme.txt"); + } + else if (nested && c == GRUB_TERM_ESC) + { + *should_exit = 1; + } +} + +static void +handle_key_events (grub_gfxmenu_model_t model, + grub_gfxmenu_view_t view, + int nested, + int *should_exit) +{ + while (!*should_exit && grub_checkkey () != -1) + { + int key = grub_getkey (); + int c = GRUB_TERM_ASCII_CHAR (key); + process_key_press (c, model, view, nested, should_exit); + } +} + +static grub_err_t +show_menu (grub_menu_t menu, int nested) +{ + grub_gfxmenu_model_t model; + + model = grub_gfxmenu_model_new (menu); + if (! model) + { + grub_print_error (); + grub_printf ("Initializing menu data for graphical menu failed;\n" + "falling back to terminal based menu.\n"); + grub_wait_after_message (); + switch_to_text_menu (); + return grub_errno; + } + + grub_gfxmenu_view_t view; + + /* Create the view. */ + const char *theme_path = grub_env_get ("theme"); + if (! theme_path) + theme_path = "/boot/grub/themes/proto/theme.txt"; + + view = grub_gfxmenu_view_new (theme_path, model); + if (! view) + { + grub_print_error (); + grub_printf ("Starting graphical menu failed;\n" + "falling back to terminal based menu.\n"); + grub_wait_after_message (); + grub_gfxmenu_model_destroy (model); + switch_to_text_menu (); + return grub_errno; + } + + /* Initially select the default menu entry. */ + int default_index = grub_menu_get_default_entry_index (menu); + grub_gfxmenu_model_set_selected_index (model, default_index); + + /* Start the timer to execute the default entry. */ + grub_gfxmenu_model_set_timeout (model); + + /* Main event loop. */ + int exit_requested = 0; + while (!exit_requested && !grub_menu_viewer_should_return ()) + { + if (grub_gfxmenu_model_timeout_expired (model)) + { + grub_gfxmenu_model_clear_timeout (model); + int i = grub_gfxmenu_model_get_selected_index (model); + grub_menu_entry_t e = grub_gfxmenu_model_get_entry (model, i); + grub_gfxmenu_view_execute_with_fallback (view, e); + continue; + } + + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + handle_key_events (model, view, nested, &exit_requested); + } + + grub_gfxmenu_view_destroy (view); + grub_gfxmenu_model_destroy (model); + + return grub_errno; +} + +static grub_err_t +grub_cmd_gfxmenu (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + grub_menu_t menu = grub_env_get_data_slot ("menu"); + if (!menu) + return grub_error (GRUB_ERR_MENU, "No menu context"); + + return show_menu (menu, 1); +} + +static struct grub_menu_viewer menu_viewer = +{ + .name = "gfxmenu", + .show_menu = show_menu +}; + +GRUB_MOD_INIT (gfxmenu) +{ + (void) mod; /* To stop warning. */ + grub_menu_viewer_register (&menu_viewer); + grub_register_command ("gfxmenu", + grub_cmd_gfxmenu, GRUB_COMMAND_FLAG_BOTH, + "gfxmenu", "Show graphical menu interface", 0); +} + +GRUB_MOD_FINI (gfxmenu) +{ + grub_unregister_command ("gfxmenu"); +} === added file 'gfxmenu/model.c' --- gfxmenu/model.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/model.c 2008-07-26 23:37:04 +0000 @@ -0,0 +1,192 @@ +/* model.c - Graphical menu interface MVC model. */ +/* + * 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 +#include + +/* Model type definition. */ +struct grub_gfxmenu_model +{ + grub_menu_t menu; + int num_entries; + grub_menu_entry_t *entries; + int selected_entry_index; + int timeout_set; + grub_uint64_t timeout_start; + grub_uint64_t timeout_at; +}; + + +grub_gfxmenu_model_t +grub_gfxmenu_model_new (grub_menu_t menu) +{ + grub_gfxmenu_model_t model; + + model = grub_malloc (sizeof (*model)); + if (! model) + return 0; + + model->menu = menu; + model->num_entries = menu->size; + model->entries = 0; + model->selected_entry_index = 0; + model->timeout_set = 0; + model->timeout_at = 0; + if (model->num_entries > 0) + { + model->entries = grub_malloc (model->num_entries + * sizeof (*model->entries)); + if (! model->entries) + goto fail_and_free; + + int i; + grub_menu_entry_t cur; + for (i = 0, cur = menu->entry_list; + i < model->num_entries; + i++, cur = cur->next) + { + model->entries[i] = cur; + } + } + + return model; + +fail_and_free: + grub_free (model->entries); + grub_free (model); + return 0; +} + +void +grub_gfxmenu_model_destroy (grub_gfxmenu_model_t model) +{ + if (! model) + return; + + grub_free (model->entries); + model->entries = 0; + + grub_free (model); +} + +grub_menu_t +grub_gfxmenu_model_get_menu (grub_gfxmenu_model_t model) +{ + return model->menu; +} + +void +grub_gfxmenu_model_set_timeout (grub_gfxmenu_model_t model) +{ + int timeout_sec = grub_menu_get_timeout (); + if (timeout_sec >= 0) + { + model->timeout_start = grub_get_time_ms (); + model->timeout_at = model->timeout_start + timeout_sec * 1000; + model->timeout_set = 1; + } + else + { + model->timeout_set = 0; + } +} + +void +grub_gfxmenu_model_clear_timeout (grub_gfxmenu_model_t model) +{ + model->timeout_set = 0; + grub_menu_set_timeout (-1); +} + +int +grub_gfxmenu_model_get_timeout_ms (grub_gfxmenu_model_t model) +{ + if (!model->timeout_set) + return -1; + + return model->timeout_at - model->timeout_start; +} + +int +grub_gfxmenu_model_get_timeout_remaining_ms (grub_gfxmenu_model_t model) +{ + if (!model->timeout_set) + return -1; + + return model->timeout_at - grub_get_time_ms (); +} + +int +grub_gfxmenu_model_timeout_expired (grub_gfxmenu_model_t model) +{ + if (model->timeout_set + && grub_get_time_ms () >= model->timeout_at) + return 1; + + return 0; +} + +int +grub_gfxmenu_model_get_num_entries (grub_gfxmenu_model_t model) +{ + return model->num_entries; +} + +int +grub_gfxmenu_model_get_selected_index (grub_gfxmenu_model_t model) +{ + return model->selected_entry_index; +} + +void +grub_gfxmenu_model_set_selected_index (grub_gfxmenu_model_t model, int index) +{ + model->selected_entry_index = index; +} + +const char * +grub_gfxmenu_model_get_entry_title (grub_gfxmenu_model_t model, int index) +{ + if (index < 0 || index >= model->num_entries) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "invalid menu index"); + return 0; + } + + return model->entries[index]->title; +} + +grub_menu_entry_t +grub_gfxmenu_model_get_entry (grub_gfxmenu_model_t model, int index) +{ + if (index < 0 || index >= model->num_entries) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "invalid menu index"); + return 0; + } + + return model->entries[index]; +} + === added file 'gfxmenu/stringutil.c' --- gfxmenu/stringutil.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/stringutil.c 2008-07-26 00:44:48 +0000 @@ -0,0 +1,196 @@ +/* stringutil.c - String utilities. */ +/* + * 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 + +/* Create a new NUL-terminated string on the heap as a substring of BUF. + The range of buf included is the half-open interval [START,END). + The index START is inclusive, END is exclusive. */ +char * +grub_new_substring (const char *buf, + grub_size_t start, grub_size_t end) +{ + char *s = grub_malloc (end - start + 1); + if (! s) + return 0; + grub_memcpy (s, buf + start, end - start); + s[end - start] = '\0'; + return s; +} + +/* Eliminate "." and ".." path elements from PATH. A new heap-allocated + string is returned. */ +static char * +canonicalize_path (const char *path) +{ + int i; + const char *p; + char *newpath = 0; + + /* Count the path components in path. */ + int components = 1; + for (p = path; *p; p++) + if (*p == '/') + components++; + + char **path_array = grub_malloc (components * sizeof (*path_array)); + if (! path_array) + return 0; + + /* Initialize array elements to NULL pointers; in case once of the + allocations fails, the cleanup code can just call grub_free() for all + pointers in the array. */ + for (i = 0; i < components; i++) + path_array[i] = 0; + + /* Parse the path into path_array. */ + p = path; + for (i = 0; i < components && p; i++) + { + /* Find the end of the path element. */ + const char *end = grub_strchr (p, '/'); + if (!end) + end = p + grub_strlen (p); + + /* Copy the element. */ + path_array[i] = grub_new_substring (p, 0, end - p); + if (!path_array[i]) + goto cleanup; + + /* Advance p to point to the start of the next element, or NULL. */ + if (*end) + p = end + 1; + else + p = 0; + } + + /* Eliminate '.' and '..' elements from the path array. */ + int newpath_length = 0; + for (i = components - 1; i >= 0; --i) + { + if (! grub_strcmp (path_array[i], ".")) + { + grub_free (path_array[i]); + path_array[i] = 0; + } + else if (! grub_strcmp (path_array[i], "..") + && i > 0) + { + /* Delete the '..' and the prior path element. */ + grub_free (path_array[i]); + path_array[i] = 0; + --i; + grub_free (path_array[i]); + path_array[i] = 0; + } + else + { + newpath_length += grub_strlen (path_array[i]) + 1; + } + } + + /* Construct a new path string. */ + newpath = grub_malloc (newpath_length + 1); + if (! newpath) + goto cleanup; + + newpath[0] = '\0'; + char *newpath_end = newpath; + int first = 1; + for (i = 0; i < components; i++) + { + char *element = path_array[i]; + if (element) + { + /* For all components but the first, prefix with a slash. */ + if (! first) + newpath_end = grub_stpcpy (newpath_end, "/"); + newpath_end = grub_stpcpy (newpath_end, element); + first = 0; + } + } + +cleanup: + for (i = 0; i < components; i++) + grub_free (path_array[i]); + grub_free (path_array); + + return newpath; +} + +/* Return a new heap-allocated string representing to absolute path + to the file referred to by PATH. If PATH is an absolute path, then + the returned path is a copy of PATH. If PATH is a relative path, then + BASE is with PATH used to construct the absolute path. */ +char * +grub_resolve_relative_path (const char *base, const char *path) +{ + char *abspath; + char *canonpath; + char *p; + + /* If PATH is an absolute path, then just use it as is. */ + if (path[0] == '/' || path[0] == '(') + return canonicalize_path (path); + + abspath = grub_malloc (grub_strlen (base) + grub_strlen (path) + 1); + if (!abspath) + return 0; + + /* Concatenate BASE and PATH. + Note that BASE is expected to have a trailing slash. */ + p = grub_stpcpy (abspath, base); + grub_stpcpy (p, path); + + canonpath = canonicalize_path (abspath); + if (!canonpath) + return abspath; + + grub_free (abspath); + return canonpath; +} + +/* Get the path of the directory where the file at FILE_PATH is located. + FILE_PATH should refer to a file, not a directory. The returned path + includes a trailing slash. + This does not handle GRUB "(hd0,0)" paths properly yet since it only + looks at slashes. */ +char * +grub_get_dirname (const char *file_path) +{ + int i; + int last_slash; + + last_slash = -1; + for (i = grub_strlen (file_path) - 1; i >= 0; --i) + { + if (file_path[i] == '/') + { + last_slash = i; + break; + } + } + if (last_slash == -1) + return grub_strdup ("/"); + + return grub_new_substring (file_path, 0, last_slash + 1); +} === added file 'gfxmenu/view.c' --- gfxmenu/view.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/view.c 2008-08-03 02:14:40 +0000 @@ -0,0 +1,1096 @@ +/* view.c - Graphical menu interface MVC view. */ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Specifies a time (in ms) to delay so the user can read the message if a + default boot attempt fails and an attempt is made to fall back on + another entry. */ +#define FALLBACK_MESSAGE_DELAY 2000 + +typedef struct grub_gfxmenu_view_icon +{ + const char *class_name; + struct grub_video_bitmap *bitmap; + struct grub_gfxmenu_view_icon *next; +} *grub_gfxmenu_view_icon_t; + +/* Definition of the private representation of the view. */ +struct grub_gfxmenu_view +{ + grub_video_rect_t screen; + + int icon_width; + int icon_height; + int item_height; + int item_padding; + int item_icon_space; + int item_spacing; + grub_font_t title_font; + grub_font_t item_font; + grub_font_t selected_item_font; + grub_font_t status_font; + char *terminal_font_name; + grub_video_color_t title_color; + grub_video_color_t item_color; + grub_video_color_t selected_item_color; + grub_video_color_t status_color; + grub_video_color_t status_bg_color; + grub_video_color_t progress_bar_border_color; + grub_video_color_t progress_bar_fg_color; + grub_video_color_t progress_bar_bg_color; + struct grub_video_bitmap *desktop_image; + grub_video_color_t desktop_color; + grub_gfxmenu_box_t menu_box; + grub_gfxmenu_box_t selected_item_box; + grub_gfxmenu_box_t terminal_box; + char *title_text; + char *progress_message_text; + char *theme_path; + /* Icon cache: linked list w/ dummy head node. */ + struct grub_gfxmenu_view_icon icon_cache; + + grub_gfxmenu_model_t model; +}; + +static void init_terminal (grub_gfxmenu_view_t view); +static void destroy_terminal (void); +static grub_err_t set_graphics_mode (void); +static grub_err_t set_text_mode (void); + +/* Create a new view object, loading the theme specified by THEME_PATH and + associating MODEL with the view. */ +grub_gfxmenu_view_t +grub_gfxmenu_view_new (const char *theme_path, grub_gfxmenu_model_t model) +{ + grub_gfxmenu_view_t view; + + view = grub_malloc (sizeof (*view)); + if (! view) + return 0; + + set_graphics_mode (); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + grub_video_get_viewport ((unsigned *) &view->screen.x, + (unsigned *) &view->screen.y, + (unsigned *) &view->screen.width, + (unsigned *) &view->screen.height); + + /* Clear the screen; there may be garbage left over in video memory, and + loading the menu style (particularly the background) can take a while. */ + grub_video_fill_rect (grub_video_map_rgb (0, 0, 0), + view->screen.x, view->screen.y, + view->screen.width, view->screen.height); + grub_video_swap_buffers (); + + grub_font_t default_font; + grub_video_color_t default_fg_color; + grub_video_color_t default_bg_color; + + default_font = grub_font_get ("Helvetica 12"); + default_fg_color = grub_video_map_rgb (0, 0, 0); + default_bg_color = grub_video_map_rgb (255, 255, 255); + + view->icon_width = 32; + view->icon_height = 32; + view->item_height = 42; + view->item_padding = 14; + view->item_icon_space = 4; + view->item_spacing = 16; + view->title_font = default_font; + view->item_font = default_font; + view->selected_item_font = default_font; + view->status_font = default_font; + view->terminal_font_name = grub_strdup ("Fixed 10"); + view->title_color = default_fg_color; + view->item_color = default_fg_color; + view->selected_item_color = default_fg_color; + view->status_color = default_bg_color; + view->status_bg_color = default_fg_color; + view->progress_bar_border_color = default_fg_color; + view->progress_bar_fg_color = grub_video_map_rgb (160, 160, 160); + view->progress_bar_bg_color = default_bg_color; + view->desktop_image = 0; + view->desktop_color = default_bg_color; + view->menu_box = 0; + view->selected_item_box = 0; + view->terminal_box = 0; + view->title_text = grub_strdup ("GRUB Boot Menu"); + view->progress_message_text = 0; + view->theme_path = 0; + view->icon_cache.class_name = 0; + view->icon_cache.bitmap = 0; + view->icon_cache.next = 0; + + view->model = model; + + if (! grub_gfxmenu_view_load_theme (view, theme_path)) + { + grub_gfxmenu_view_destroy (view); + return 0; + } + + init_terminal (view); + + return view; +} + +static void +free_icon_cache (grub_gfxmenu_view_t view) +{ + grub_gfxmenu_view_icon_t cur; + grub_gfxmenu_view_icon_t next; + for (cur = view->icon_cache.next; cur; cur = next) + { + next = cur->next; + grub_free (cur); + } + view->icon_cache.next = 0; +} + +/* Destroy the view object. All used memory is freed. */ +void +grub_gfxmenu_view_destroy (grub_gfxmenu_view_t view) +{ + grub_video_bitmap_destroy (view->desktop_image); + if (view->menu_box) + view->menu_box->destroy (view->menu_box); + if (view->selected_item_box) + view->selected_item_box->destroy (view->selected_item_box); + if (view->terminal_box) + view->terminal_box->destroy (view->terminal_box); + grub_free (view->terminal_font_name); + grub_free (view->title_text); + grub_free (view->progress_message_text); + grub_free (view->theme_path); + free_icon_cache (view); + grub_free (view); + + set_text_mode (); + destroy_terminal (); +} + +/* Sets MESSAGE as the progress message for the view. The string MESSAGE + must be heap-allocated and will be owned by VIEW. MESSAGE can be 0, in + which case no message is displayed. */ +static void +set_progress_message (grub_gfxmenu_view_t view, char *message) +{ + grub_free (view->progress_message_text); + view->progress_message_text = message; +} + +/* Parse a color string of the form "r, g, b". + Whitespace is insignificant. */ +static grub_video_color_t +parse_color (const char *s) +{ + int red = grub_strtoul (s, 0, 0); + if ((s = grub_strchr (s, ',')) == 0) + goto fail; + s++; + int green = grub_strtoul (s, 0, 0); + if ((s = grub_strchr (s, ',')) == 0) + goto fail; + s++; + int blue = grub_strtoul (s, 0, 0); + int alpha; + if ((s = grub_strchr (s, ',')) == 0) + alpha = 255; + else + { + s++; + alpha = grub_strtoul (s, 0, 0); + } + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + return grub_video_map_rgba (red, green, blue, alpha); + +fail: + return 0; +} + +/* Construct a new box widget using PATTERN to find the pixmap files for it, + storing the new widget at *BOXPTR. PATTERN should be of the form: + "somewhere/style*.png". The '*' then gets substituted with the various + pixmap names that the widget uses. + Return zero on failure, nonzero on success. */ +static int +theme_set_box (grub_gfxmenu_box_t *boxptr, + char *pattern, const char *theme_dir) +{ + char *abspattern; + char *prefix; + char *suffix; + char *star; + grub_gfxmenu_box_t box; + + abspattern = grub_resolve_relative_path (theme_dir, pattern); + if (! abspattern) + return 0; + + star = grub_strchr (abspattern, '*'); + if (! star) + { + grub_free (abspattern); + return 0; + } + + /* Prefix: Get the part before the '*'. */ + prefix = grub_malloc (star - abspattern + 1); + if (! prefix) + { + grub_free (abspattern); + return 0; + } + + grub_memcpy (prefix, abspattern, star - abspattern); + prefix[star - abspattern] = '\0'; + + /* Suffix: Everything after the '*' is the suffix. */ + suffix = star + 1; + + box = grub_gfxmenu_create_box (prefix, suffix); + grub_free (abspattern); /* Note: suffix, star point into abspattern. */ + grub_free (prefix); + if (! box) + return 0; + + if (*boxptr) + (*boxptr)->destroy (*boxptr); + *boxptr = box; + return 1; + +} + +/* Set the specified property NAME on the view to the given string VALUE. + This function takes ownership of both NAME and VALUE, so the caller + should pass pointers to new heap-allocated strings. */ +static void +theme_set_string (grub_gfxmenu_view_t view, char *name, char *value, + const char *theme_dir) +{ + if (! grub_strcmp ("title-font", name)) + view->title_font = grub_font_get (value); + else if (! grub_strcmp ("item-font", name)) + view->item_font = grub_font_get (value); + else if (! grub_strcmp ("selected-item-font", name)) + view->selected_item_font = grub_font_get (value); + else if (! grub_strcmp ("status-font", name)) + view->status_font = grub_font_get (value); + else if (! grub_strcmp ("terminal-font", name)) + { + grub_free (view->terminal_font_name); + view->terminal_font_name = value; + value = 0; /* Prevent value from being freed below. */ + } + else if (! grub_strcmp ("title-color", name)) + view->title_color = parse_color (value); + else if (! grub_strcmp ("item-color", name)) + view->item_color = parse_color (value); + else if (! grub_strcmp ("selected-item-color", name)) + view->selected_item_color = parse_color (value); + else if (! grub_strcmp ("status-color", name)) + view->status_color = parse_color (value); + else if (! grub_strcmp ("status-bg-color", name)) + view->status_bg_color = parse_color (value); + else if (! grub_strcmp ("progress-bar-border-color", name)) + view->progress_bar_border_color = parse_color (value); + else if (! grub_strcmp ("progress-bar-fg-color", name)) + view->progress_bar_fg_color = parse_color (value); + else if (! grub_strcmp ("progress-bar-bg-color", name)) + view->progress_bar_bg_color = parse_color (value); + else if (! grub_strcmp ("desktop-image", name)) + { + struct grub_video_bitmap *raw_bitmap; + struct grub_video_bitmap *scaled_bitmap; + char *path; + path = grub_resolve_relative_path (theme_dir, value); + if (! path) + goto fail; + if (grub_video_bitmap_load (&raw_bitmap, path) != GRUB_ERR_NONE) + { + grub_free (path); + goto fail; + } + grub_free(path); + grub_video_bitmap_create_scaled (&scaled_bitmap, + view->screen.width, + view->screen.height, + raw_bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + grub_video_bitmap_destroy (raw_bitmap); + if (!scaled_bitmap) + goto fail; + + grub_video_bitmap_destroy (view->desktop_image); + view->desktop_image = scaled_bitmap; + } + else if (! grub_strcmp ("desktop-color", name)) + view->desktop_color = parse_color (value); + else if (! grub_strcmp ("menu-box", name)) + theme_set_box (&view->menu_box, value, theme_dir); + else if (! grub_strcmp ("selected-item-box", name)) + theme_set_box (&view->selected_item_box, value, theme_dir); + else if (! grub_strcmp ("terminal-box", name)) + theme_set_box (&view->terminal_box, value, theme_dir); + else if (! grub_strcmp ("title-text", name)) + { + grub_free (view->title_text); + view->title_text = value; + value = 0; /* Prevent value from being freed below. */ + } + +fail: + grub_free (value); + grub_free (name); +} + +/* Set the specified property NAME on the view to the given numeric VALUE. + This function takes ownership NAME, so the caller should pass a pointer + to a new heap-allocated string. */ +static void +theme_set_number (grub_gfxmenu_view_t view, char *name, int value) +{ + if (! grub_strcmp ("icon-width", name)) + view->icon_width = value; + else if (! grub_strcmp ("icon-height", name)) + view->icon_height = value; + else if (! grub_strcmp ("item-height", name)) + view->item_height = value; + else if (! grub_strcmp ("item-padding", name)) + view->item_padding = value; + else if (! grub_strcmp ("item-icon-space", name)) + view->item_icon_space = value; + else if (! grub_strcmp ("item-spacing", name)) + view->item_spacing = value; + + grub_free (name); +} + +/* Set properties on the view based on settings from the specified + theme file. Returns nonzero on success, zero on failure. */ +int +grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, const char *theme_path) +{ + char *theme_dir; + grub_file_t file; + char *buf; + int pos; + int len; + + theme_dir = grub_get_dirname (theme_path); + + file = grub_file_open (theme_path); + if (!file) + { + grub_free (theme_dir); + return 0; + } + + len = grub_file_size (file); + buf = grub_malloc (len); + if (! buf) + { + grub_file_close (file); + grub_free (theme_dir); + return 0; + } + if (grub_file_read (file, buf, len) != len) + { + grub_free (buf); + grub_file_close (file); + grub_free (theme_dir); + return 0; + } + + pos = 0; + while (pos < len) + { + /* Skip comments (lines beginning with #). */ + if (pos < len && buf[pos] == '#') + goto nextline; + + /* Get name. */ + /* Find a word character. */ + while (pos < len && grub_isspace(buf[pos])) + pos++; + int name_start = pos; + /* Find the end of the name. */ + while (pos < len + && (grub_isalpha(buf[pos]) + || grub_isdigit(buf[pos]) + || buf[pos] == '_' + || buf[pos] == '-')) + pos++; + int name_end = pos; + + if (name_end - name_start < 1) + goto nextline; + + /* Skip whitespace before separator. */ + while (pos < len + && (buf[pos] == ' ' + || buf[pos] == '\t' + || buf[pos] == '\f')) + pos++; + + /* Read separator. */ + if (buf[pos] != ':') + goto nextline; + + pos++; /* Skip separator. */ + + /* Skip whitespace after separator. */ + while (pos < len + && (buf[pos] == ' ' + || buf[pos] == '\t' + || buf[pos] == '\f')) + pos++; + + /* Get the value based on its type. */ + if (pos < len && buf[pos] == '"') + { + /* String value. (e.g., '"My string"') */ + + int value_start; + int value_end; + + /* Skip the opening quotation mark. */ + pos++; + /* Get string value. */ + value_start = pos; + /* Find the ending quotation mark. */ + while (pos < len + && !(buf[pos] == '"' + || buf[pos] == '\n')) + pos++; + value_end = pos; + theme_set_string (view, + grub_new_substring (buf, name_start, name_end), + grub_new_substring (buf, value_start, value_end), + theme_dir); + } + else if (pos < len && grub_isdigit(buf[pos])) + { + /* Numeric value. (e.g., '123') */ + + int value_start; + int value_end; + char *value_str; + int value; + + /* Get numeric value. */ + value_start = pos; + /* Find the end of the digit sequence. */ + while (pos < len && grub_isdigit(buf[pos])) + pos++; + value_end = pos; + value_str = grub_new_substring (buf, value_start, value_end); + if (!value_str) + continue; + value = grub_strtoul (value_str, 0, 0); + grub_free (value_str); + theme_set_number (view, + grub_new_substring (buf, name_start, name_end), + value); + } + +nextline: + /* Eat characters up to the newline. */ + while (pos < len && buf[pos] != '\n') + pos++; + pos++; /* Eat the newline. */ + } + + grub_free (view->theme_path); + view->theme_path = grub_strdup (theme_path); + free_icon_cache (view); /* There may be new icons for this theme. */ + grub_free (buf); + grub_file_close (file); + grub_free (theme_dir); + return 1; +} + +static void +draw_background (grub_gfxmenu_view_t view) +{ + if (view->desktop_image) + { + struct grub_video_bitmap *img = view->desktop_image; + grub_video_blit_bitmap (img, GRUB_VIDEO_BLIT_REPLACE, + view->screen.x, view->screen.y, 0, 0, + grub_video_bitmap_get_width (img), + grub_video_bitmap_get_height (img)); + } + else + { + grub_video_fill_rect (view->desktop_color, + view->screen.x, view->screen.y, + view->screen.width, view->screen.height); + } +} + +static const char icon_extension[] = ".png"; + +static struct grub_video_bitmap * +try_loading_icon (grub_gfxmenu_view_t view, + const char *dir, const char *class_name) +{ + char *path = grub_malloc (grub_strlen (dir) + + grub_strlen (class_name) + + grub_strlen (icon_extension) + + 100); + if (! path) + return 0; + + grub_strcpy (path, dir); + grub_strcat (path, class_name); + grub_strcat (path, icon_extension); + + struct grub_video_bitmap *raw_bitmap; + grub_video_bitmap_load (&raw_bitmap, path); + grub_free (path); + grub_errno = GRUB_ERR_NONE; /* Critical to clear the error!! */ + if (! raw_bitmap) + return 0; + + struct grub_video_bitmap *scaled_bitmap; + grub_video_bitmap_create_scaled (&scaled_bitmap, + view->icon_width, view->icon_height, + raw_bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + grub_video_bitmap_destroy (raw_bitmap); + if (! scaled_bitmap) + return 0; + + return scaled_bitmap; +} + +static struct grub_video_bitmap * +get_icon_by_class (grub_gfxmenu_view_t view, const char *class_name) +{ + + /* First check the view's icon cache. */ + grub_gfxmenu_view_icon_t vicon; + for (vicon = view->icon_cache.next; vicon; vicon = vicon->next) + { + if (grub_strcmp (vicon->class_name, class_name) == 0) + return vicon->bitmap; + } + + /* Otherwise, we search for an icon to load. */ + if (! view->theme_path) + return 0; + + char *theme_dir = grub_get_dirname (view->theme_path); + char *icons_dir; + struct grub_video_bitmap *icon; + icon = 0; + /* First try the theme's own icons, from "grub/themes/NAME/icons/" */ + icons_dir = grub_resolve_relative_path (theme_dir, "icons/"); + if (icons_dir) + { + icon = try_loading_icon (view, icons_dir, class_name); + grub_free (icons_dir); + } + if (! icon) + { + /* If the theme doesn't have an appropriate icon, check in + "grub/themes/icons". */ + icons_dir = grub_resolve_relative_path (theme_dir, "../icons/"); + if (icons_dir) + { + icon = try_loading_icon (view, icons_dir, class_name); + grub_free (icons_dir); + } + } + grub_free (theme_dir); + + if (! icon) + return 0; + + vicon = grub_malloc (sizeof (*vicon)); + if (! vicon) + { + grub_video_bitmap_destroy (icon); + return 0; + } + vicon->class_name = class_name; + vicon->bitmap = icon; + vicon->next = view->icon_cache.next; + view->icon_cache.next = vicon; /* Link it into the cache. */ + return vicon->bitmap; +} + +static struct grub_video_bitmap * +get_item_icon (grub_gfxmenu_view_t view, int item_index) +{ + grub_menu_entry_t entry; + entry = grub_gfxmenu_model_get_entry (view->model, item_index); + if (! entry) + return 0; + + struct grub_menu_entry_class *c; + struct grub_video_bitmap *icon; + icon = 0; + for (c = entry->classes->next; c && ! icon; c = c->next) + icon = get_icon_by_class (view, c->name); + return icon; +} + +static void +draw_menu (grub_gfxmenu_view_t view) +{ + int boxpad = view->item_padding; + int icon_text_space = view->item_icon_space; + int item_vspace = view->item_spacing; + + int ascent = grub_font_get_ascent (view->item_font); + int descent = grub_font_get_descent (view->item_font); + int item_height = view->item_height; + + int num_items = grub_gfxmenu_model_get_num_entries (view->model); + grub_video_rect_t r; + r.width = view->screen.width * 4 / 5; + /* Set the menu box height to fit the items. */ + r.height = (item_height * num_items + + item_vspace * (num_items - 1) + + 2 * boxpad); + r.x = (view->screen.width - r.width) / 2; + r.y = (view->screen.height - r.height) / 2; + view->menu_box->set_content_size (view->menu_box, r.width, r.height); + + int menu_box_left_pad = view->menu_box->get_left_pad (view->menu_box); + int menu_box_top_pad = view->menu_box->get_top_pad (view->menu_box); + view->menu_box->draw (view->menu_box, + r.x - menu_box_left_pad, r.y - menu_box_top_pad); + + int item_top = r.y + boxpad; + int item_left = r.x + boxpad; + int i; + + for (i = 0; i < num_items; i++) + { + int is_selected = + (i == grub_gfxmenu_model_get_selected_index (view->model)); + + if (is_selected) + { + view->selected_item_box->set_content_size (view->selected_item_box, + r.width - 2 * boxpad, + item_height); + int leftpad = view->selected_item_box->get_left_pad (view->selected_item_box); + int toppad = view->selected_item_box->get_top_pad (view->selected_item_box); + view->selected_item_box->draw (view->selected_item_box, + item_left - leftpad, + item_top - toppad); + } + + struct grub_video_bitmap *icon; + if ((icon = get_item_icon (view, i)) != 0) + grub_video_blit_bitmap (icon, GRUB_VIDEO_BLIT_BLEND, + item_left, + item_top + (item_height - view->icon_height) / 2, + 0, 0, view->icon_width, view->icon_height); + + const char *item_title = + grub_gfxmenu_model_get_entry_title (view->model, i); + grub_video_draw_string (item_title, + (is_selected + ? view->selected_item_font + : view->item_font), + (is_selected + ? view->selected_item_color + : view->item_color), + item_left + view->icon_width + icon_text_space, + (item_top + (item_height - (ascent + descent)) + / 2 + ascent)); + + item_top += item_height + item_vspace; + } +} + +static void +draw_title (grub_gfxmenu_view_t view) +{ + if (! view->title_text) + return; + + /* Center the title. */ + int title_width = grub_font_get_string_width (view->title_font, + view->title_text); + int x = (view->screen.width - title_width) / 2; + int y = 40 + grub_font_get_ascent (view->title_font); + grub_video_draw_string (view->title_text, + view->title_font, view->title_color, + x, y); +} + +static void +draw_status (grub_gfxmenu_view_t view) +{ + int descent = grub_font_get_descent (view->status_font); + int ascent = grub_font_get_ascent (view->status_font); + int vpad = 5; + int textheight = descent + ascent + 1; + int h = 2 * vpad + 2 * textheight; + + grub_video_fill_rect (view->status_bg_color, + 0, view->screen.height - h, + view->screen.width, view->screen.height - 1); + + int texty = view->screen.height - h + vpad + ascent; + grub_video_draw_string ("Select an item with the arrow keys and " + "press Enter to boot.", + view->status_font, view->status_color, 30, texty); + texty += textheight; + grub_video_draw_string ("Press: 'c' for command line; 't' to switch to " + "non-graphical menu.", + view->status_font, view->status_color, 30, texty); +} + +static void +draw_timeout (grub_gfxmenu_view_t view) +{ + int timeout = grub_gfxmenu_model_get_timeout_ms (view->model); + if (timeout == -1) + return; + + int remaining = grub_gfxmenu_model_get_timeout_remaining_ms (view->model); + if (remaining < 0) + remaining = 0; + + int t = timeout - remaining; + + /* Set the timeout bar's frame. */ + grub_video_rect_t f; + f.width = view->screen.width * 3 / 5; + f.height = view->screen.height / 25; + f.x = view->screen.x + (view->screen.width - f.width) / 2; + f.y = view->screen.y + view->screen.height - 90; + + /* First attempt just uses filled rectangles; + TODO we should enhance with a pixmap themed progress bar component. */ + + /* Border. */ + grub_video_fill_rect (view->progress_bar_border_color, + f.x - 1, f.y - 1, + f.width + 2, f.height + 2); + + /* Bar background. */ + int barwidth = f.width * t / timeout; + grub_video_fill_rect (view->progress_bar_bg_color, + f.x + barwidth, f.y, + f.width - barwidth, f.height); + + /* Bar foreground. */ + grub_video_fill_rect (view->progress_bar_fg_color, + f.x, f.y, + barwidth, f.height); + + char *text = grub_malloc (200); + if (!text) + return; + int seconds_remaining_rounded_up = (remaining + 999) / 1000; + grub_sprintf (text, + "The highlighted entry will be booted automatically in %d s.", + seconds_remaining_rounded_up); + grub_font_t font = view->status_font; + grub_video_color_t color = view->progress_bar_border_color; + + /* Center the text. */ + int text_width = grub_font_get_string_width (font, text); + int x = f.x + (f.width - text_width) / 2; + int y = (f.y + (f.height - grub_font_get_descent (font)) / 2 + + grub_font_get_ascent (font) / 2); + grub_video_draw_string (text, font, color, x, y); + grub_free (text); +} + +static void +draw_message (grub_gfxmenu_view_t view) +{ + char *text = view->progress_message_text; + if (! text) + return; + + grub_font_t font = view->status_font; + grub_video_color_t color = view->status_color; + + /* Set the timeout bar's frame. */ + grub_video_rect_t f; + f.width = view->screen.width * 4 / 5; + f.height = 50; + f.x = view->screen.x + (view->screen.width - f.width) / 2; + f.y = view->screen.y + view->screen.height - 90 - 20 - f.height; + + /* Border. */ + grub_video_fill_rect (color, + f.x-1, f.y-1, f.width+2, f.height+2); + /* Fill. */ + grub_video_fill_rect (view->status_bg_color, + f.x, f.y, f.width, f.height); + + /* Center the text. */ + int text_width = grub_font_get_string_width (font, text); + int x = f.x + (f.width - text_width) / 2; + int y = (f.y + (f.height - grub_font_get_descent (font)) / 2 + + grub_font_get_ascent (font) / 2); + grub_video_draw_string (text, font, color, x, y); +} + + +void +grub_gfxmenu_view_draw (grub_gfxmenu_view_t view) +{ + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + draw_background (view); + draw_menu (view); + draw_title (view); + draw_status (view); + draw_timeout (view); + draw_message (view); +} + +static grub_err_t +set_graphics_mode (void) +{ + const char *doublebuf_str = grub_env_get ("doublebuffering"); + int doublebuf_flags = + (doublebuf_str && doublebuf_str[0] == 'n') + ? 0 + : GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + + const char *modestr = grub_env_get ("gfxmode"); + if (grub_video_setup_preferred_mode (modestr, doublebuf_flags, 640, 480) + != GRUB_ERR_NONE) + return grub_errno; + + return GRUB_ERR_NONE; +} + +static grub_err_t +set_text_mode (void) +{ + return grub_video_restore (); +} + +static int term_target_width; +static int term_target_height; +static struct grub_video_render_target *term_target; +static int term_initialized; +static grub_term_t term_original; +static grub_gfxmenu_view_t term_view; + +static void +repaint_terminal (int x __attribute ((unused)), + int y __attribute ((unused)), + int width __attribute ((unused)), + int height __attribute ((unused))) +{ + if (! term_view) + return; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + grub_gfxmenu_view_draw (term_view); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + int termx = term_view->screen.x + + term_view->screen.width * (10 - 7) / 10 / 2; + int termy = term_view->screen.y + + term_view->screen.height * (10 - 7) / 10 / 2; + + grub_gfxmenu_box_t term_box = term_view->terminal_box; + if (term_box) + { + term_box->set_content_size (term_box, + term_target_width, term_target_height); + + term_box->draw (term_box, + termx - term_box->get_left_pad (term_box), + termy - term_box->get_top_pad (term_box)); + } + + grub_video_blit_render_target (term_target, GRUB_VIDEO_BLIT_REPLACE, + termx, termy, + 0, 0, term_target_width, term_target_height); + grub_video_swap_buffers (); +} + +static void +init_terminal (grub_gfxmenu_view_t view) +{ + term_original = grub_term_get_current (); + + term_target_width = view->screen.width * 7 / 10; + term_target_height = view->screen.height * 7 / 10; + + grub_video_create_render_target (&term_target, + term_target_width, + term_target_height, + GRUB_VIDEO_MODE_TYPE_RGB + | GRUB_VIDEO_MODE_TYPE_ALPHA); + if (grub_errno != GRUB_ERR_NONE) + return; + + /* Note: currently there is no API for changing the gfxterm font + on the fly, so whatever font the initially loaded theme specifies + will be permanent. */ + grub_gfxterm_init_window (term_target, 0, 0, + term_target_width, term_target_height, + view->terminal_font_name, 3); + if (grub_errno != GRUB_ERR_NONE) + return; + term_initialized = 1; + + /* XXX: store static pointer to the 'view' object so the repaint callback can access it. */ + term_view = view; + grub_gfxterm_set_repaint_callback (repaint_terminal); + grub_term_set_current (grub_gfxterm_get_term ()); +} + +static void destroy_terminal (void) +{ + term_view = 0; + if (term_initialized) + grub_gfxterm_destroy_window (); + grub_gfxterm_set_repaint_callback (0); + if (term_target) + grub_video_delete_render_target (term_target); + if (term_original) + grub_term_set_current (term_original); +} + + +static void +notify_booting (void *userdata, grub_menu_entry_t entry) +{ + grub_gfxmenu_view_t view = (grub_gfxmenu_view_t) userdata; + + char *s = grub_malloc (100 + grub_strlen (entry->title)); + if (!s) + return; + + grub_sprintf (s, "Booting '%s'", entry->title); + set_progress_message (view, s); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); +} + +static void +notify_fallback (void *userdata, grub_menu_entry_t entry) +{ + grub_gfxmenu_view_t view = (grub_gfxmenu_view_t) userdata; + + char *s = grub_malloc (100 + grub_strlen (entry->title)); + if (!s) + return; + + grub_sprintf (s, "Falling back to '%s'", entry->title); + set_progress_message (view, s); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + grub_millisleep (FALLBACK_MESSAGE_DELAY); +} + +static void +notify_execution_failure (void *userdata __attribute__ ((unused))) +{ +} + + +static struct grub_menu_execute_callback execute_callback = +{ + .notify_booting = notify_booting, + .notify_fallback = notify_fallback, + .notify_failure = notify_execution_failure +}; + +int +grub_gfxmenu_view_execute_with_fallback (grub_gfxmenu_view_t view, + grub_menu_entry_t entry) +{ + grub_menu_execute_with_fallback (grub_gfxmenu_model_get_menu (view->model), + entry, &execute_callback, (void *) view); + + if (set_graphics_mode () != GRUB_ERR_NONE) + return 0; /* Failure. */ + + /* If we returned, there was a failure. */ + set_progress_message (view, grub_strdup ("Unable to automatically boot. " + "Press SPACE to continue.")); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + while (GRUB_TERM_ASCII_CHAR(grub_getkey ()) != ' ') + { + /* Wait for SPACE to be pressed. */ + } + + set_progress_message (view, 0); /* Clear the message. */ + + return 1; /* Ok. */ +} + +int +grub_gfxmenu_view_execute_entry (grub_gfxmenu_view_t view, + grub_menu_entry_t entry) +{ + /* Currently we switch back to text mode by restoring + the original terminal before executing the menu entry. + It is hard to make it work when executing a menu entry + that switches video modes -- it using gfxterm in a + window, the repaint callback seems to crash GRUB. */ + /* TODO: Determine if this works when 'gfxterm' was set as + the current terminal before invoking the gfxmenu. */ + destroy_terminal (); + + grub_menu_execute_entry (entry); + if (grub_errno != GRUB_ERR_NONE) + grub_wait_after_message (); + + if (set_graphics_mode () != GRUB_ERR_NONE) + return 0; /* Failure. */ + + init_terminal (view); + return 1; /* Ok. */ +} + +void +grub_gfxmenu_view_run_terminal (grub_gfxmenu_view_t view __attribute__((unused))) +{ + grub_cmdline_run (1); +} + === added file 'gfxmenu/widget-box.c' --- gfxmenu/widget-box.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/widget-box.c 2008-07-19 18:25:18 +0000 @@ -0,0 +1,244 @@ +/* widget_box.c - Pixmap-stylized box widget. */ +/* + * 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 +#include + +enum box_pixmaps +{ + BOX_PIXMAP_NW, BOX_PIXMAP_NE, BOX_PIXMAP_SE, BOX_PIXMAP_SW, + BOX_PIXMAP_N, BOX_PIXMAP_E, BOX_PIXMAP_S, BOX_PIXMAP_W, + BOX_PIXMAP_CENTER +}; + +static const char *box_pixmap_names[] = { + /* Corners: */ + "nw", "ne", "se", "sw", + /* Sides: */ + "n", "e", "s", "w", + /* Center: */ + "c" +}; + +#define BOX_NUM_PIXMAPS (sizeof(box_pixmap_names)/sizeof(*box_pixmap_names)) + +static void +draw (grub_gfxmenu_box_t self, int x, int y) +{ + int height_n; + int height_s; + int height_e; + int height_w; + int width_n; + int width_s; + int width_e; + int width_w; + unsigned i; + + /* Don't try to draw if any pixmaps are null, except the center. */ + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + if (i != BOX_PIXMAP_CENTER && self->scaled_pixmaps[i] == 0) + return; + } + + height_n = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_N]); + height_s = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_S]); + height_e = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_E]); + height_w = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_W]); + width_n = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_N]); + width_s = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_S]); + width_e = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_E]); + width_w = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_W]); + + /* Draw sides. */ + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_N], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y, 0, 0, width_n, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_S], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y + height_n + height_w, + 0, 0, width_s, height_s); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_E], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y + height_n, + 0, 0, width_e, height_e); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_W], + GRUB_VIDEO_BLIT_BLEND, + x, y + height_n, 0, 0, width_w, height_w); + + /* Draw corners. */ + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_NW], + GRUB_VIDEO_BLIT_BLEND, + x, y, 0, 0, width_w, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_NE], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y, + 0, 0, width_e, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_SE], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y + height_n + height_e, + 0, 0, width_e, height_s); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_SW], + GRUB_VIDEO_BLIT_BLEND, + x, y + height_n + height_w, + 0, 0, width_w, height_s); + + /* Draw center. */ + if (self->scaled_pixmaps[BOX_PIXMAP_CENTER]) + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_CENTER], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y + height_n, + 0, 0, self->width, self->height); +} + +static void +scale_pixmap (grub_gfxmenu_box_t self, int i, int w, int h) +{ + struct grub_video_bitmap **scaled = &self->scaled_pixmaps[i]; + struct grub_video_bitmap *raw = self->raw_pixmaps[i]; + + if (raw == 0) + return; + + if (w == -1) + w = grub_video_bitmap_get_width (raw); + if (h == -1) + h = grub_video_bitmap_get_height (raw); + + if (*scaled == 0 + || ((int) grub_video_bitmap_get_width (*scaled) != w) + || ((int) grub_video_bitmap_get_height (*scaled) != h)) + { + if (*scaled) + { + grub_video_bitmap_destroy (*scaled); + *scaled = 0; + } + grub_video_bitmap_create_scaled (scaled, w, h, raw, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + } +} + +static void +set_content_size (grub_gfxmenu_box_t self, + int width, int height) +{ + self->width = width; + self->height = height; + /* Resize sides to match the width and height. */ + /* It is assumed that the corners width/height match the adjacent sides. */ + + /* Resize N and S sides to match width. */ + scale_pixmap(self, BOX_PIXMAP_N, width, -1); + scale_pixmap(self, BOX_PIXMAP_S, width, -1); + + /* Resize E and W sides to match height. */ + scale_pixmap(self, BOX_PIXMAP_E, -1, height); + scale_pixmap(self, BOX_PIXMAP_W, -1, height); + + /* Don't scale the corners--they are assumed to match the sides. */ + scale_pixmap(self, BOX_PIXMAP_NW, -1, -1); + scale_pixmap(self, BOX_PIXMAP_SW, -1, -1); + scale_pixmap(self, BOX_PIXMAP_NE, -1, -1); + scale_pixmap(self, BOX_PIXMAP_SE, -1, -1); + + /* Scale the center area. */ + scale_pixmap(self, BOX_PIXMAP_CENTER, width, height); +} + +static int +get_left_pad (grub_gfxmenu_box_t self) +{ + return grub_video_bitmap_get_width (self->raw_pixmaps[BOX_PIXMAP_W]); +} + +static int +get_top_pad (grub_gfxmenu_box_t self) +{ + return grub_video_bitmap_get_height (self->raw_pixmaps[BOX_PIXMAP_N]); +} + +static void +destroy (grub_gfxmenu_box_t self) +{ + unsigned i; + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + if (self->raw_pixmaps[i]) + grub_video_bitmap_destroy(self->raw_pixmaps[i]); + self->raw_pixmaps[i] = 0; + + if (self->scaled_pixmaps[i]) + grub_video_bitmap_destroy(self->scaled_pixmaps[i]); + self->scaled_pixmaps[i] = 0; + } + grub_free (self->raw_pixmaps); + self->raw_pixmaps = 0; + grub_free (self->scaled_pixmaps); + self->scaled_pixmaps = 0; + + grub_free (self); /* Free self: must be the last step! */ +} + + +grub_gfxmenu_box_t +grub_gfxmenu_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix) +{ + char path[200]; + unsigned i; + grub_gfxmenu_box_t box; + + box = (grub_gfxmenu_box_t) grub_malloc (sizeof (*box)); + if (!box) + return 0; + box->width = 0; + box->height = 0; + box->raw_pixmaps = + (struct grub_video_bitmap **) + grub_malloc (BOX_NUM_PIXMAPS * sizeof (struct grub_video_bitmap *)); + box->scaled_pixmaps = + (struct grub_video_bitmap **) + grub_malloc (BOX_NUM_PIXMAPS * sizeof (struct grub_video_bitmap *)); + + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + /* TODO XXX dynamically allocate PATH, making sure it's large enough */ + grub_sprintf (path, "%s%s%s", + pixmaps_prefix, box_pixmap_names[i], pixmaps_suffix); + grub_video_bitmap_load (&box->raw_pixmaps[i], path); + box->scaled_pixmaps[i] = 0; + } + + box->draw = draw; + box->set_content_size = set_content_size; + box->get_left_pad = get_left_pad; + box->get_top_pad = get_top_pad; + box->destroy = destroy; + + return box; +} + === 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 */ + === added file 'include/grub/gfxmenu_model.h' --- include/grub/gfxmenu_model.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxmenu_model.h 2008-07-26 23:37:04 +0000 @@ -0,0 +1,59 @@ +/* gfxmenu_model.h - gfxmenu model interface. */ +/* + * 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 GRUB_GFXMENU_MODEL_HEADER +#define GRUB_GFXMENU_MODEL_HEADER 1 + +#include + +struct grub_gfxmenu_model; /* Forward declaration of opaque type. */ +typedef struct grub_gfxmenu_model *grub_gfxmenu_model_t; + + +grub_gfxmenu_model_t grub_gfxmenu_model_new (grub_menu_t menu); + +void grub_gfxmenu_model_destroy (grub_gfxmenu_model_t model); + +grub_menu_t grub_gfxmenu_model_get_menu (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_set_timeout (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_clear_timeout (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_timeout_ms (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_timeout_remaining_ms (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_timeout_expired (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_num_entries (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_selected_index (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_set_selected_index (grub_gfxmenu_model_t model, + int index); + +const char *grub_gfxmenu_model_get_entry_title (grub_gfxmenu_model_t model, + int index); + +grub_menu_entry_t grub_gfxmenu_model_get_entry (grub_gfxmenu_model_t model, + int index); + +#endif /* GRUB_GFXMENU_MODEL_HEADER */ + === added file 'include/grub/gfxmenu_view.h' --- include/grub/gfxmenu_view.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxmenu_view.h 2008-07-26 23:37:04 +0000 @@ -0,0 +1,53 @@ +/* gfxmenu_view.h - gfxmenu view interface. */ +/* + * 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 GRUB_GFXMENU_VIEW_HEADER +#define GRUB_GFXMENU_VIEW_HEADER 1 + +#include +#include +#include +#include + +struct grub_gfxmenu_view; /* Forward declaration of opaque type. */ +typedef struct grub_gfxmenu_view *grub_gfxmenu_view_t; + + +grub_gfxmenu_view_t grub_gfxmenu_view_new (const char *theme_path, + grub_gfxmenu_model_t model); + +void grub_gfxmenu_view_destroy (grub_gfxmenu_view_t view); + +/* Set properties on the view based on settings from the specified + theme file. */ +int grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, + const char *theme_path); + +void grub_gfxmenu_view_draw (grub_gfxmenu_view_t view); + +int grub_gfxmenu_view_execute_with_fallback (grub_gfxmenu_view_t view, + grub_menu_entry_t entry); + +int grub_gfxmenu_view_execute_entry (grub_gfxmenu_view_t view, + grub_menu_entry_t entry); + +void grub_gfxmenu_view_run_terminal (grub_gfxmenu_view_t view); + +#endif /* GRUB_GFXMENU_VIEW_HEADER */ + === added file 'include/grub/gfxterm.h' --- include/grub/gfxterm.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxterm.h 2008-07-19 19:56:06 +0000 @@ -0,0 +1,41 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 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_GFXTERM_HEADER +#define GRUB_GFXTERM_HEADER 1 + +#include +#include +#include +#include + +grub_err_t +grub_gfxterm_init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width); + +void grub_gfxterm_destroy_window (void); + +grub_term_t grub_gfxterm_get_term (void); + +typedef void (*grub_gfxterm_repaint_callback_t)(int x, int y, + int width, int height); + +void grub_gfxterm_set_repaint_callback (grub_gfxterm_repaint_callback_t func); + +#endif /* ! GRUB_GFXTERM_HEADER */ === added file 'include/grub/gfxwidgets.h' --- include/grub/gfxwidgets.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxwidgets.h 2008-07-19 18:25:18 +0000 @@ -0,0 +1,47 @@ +/* gfxwidgets.h - Widgets for the graphical menu (gfxmenu). */ +/* + * 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 GRUB_GFXWIDGETS_HEADER +#define GRUB_GFXWIDGETS_HEADER 1 + +#include + +typedef struct grub_gfxmenu_box *grub_gfxmenu_box_t; + +struct grub_gfxmenu_box +{ + /* The size of the content. */ + int width; + int height; + + struct grub_video_bitmap **raw_pixmaps; + struct grub_video_bitmap **scaled_pixmaps; + + void (*draw) (grub_gfxmenu_box_t self, int x, int y); + void (*set_content_size) (grub_gfxmenu_box_t self, + int width, int height); + int (*get_left_pad) (grub_gfxmenu_box_t self); + int (*get_top_pad) (grub_gfxmenu_box_t self); + void (*destroy) (grub_gfxmenu_box_t self); +}; + +grub_gfxmenu_box_t grub_gfxmenu_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix); + +#endif /* ! GRUB_GFXWIDGETS_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-09 17:27:53 +0000 @@ -25,55 +25,93 @@ 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_BGRA8888_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_BGR888_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_BGR888_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_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-28 16:23:46 +0000 @@ -0,0 +1,80 @@ +/* + * 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; +} + +static __inline int +grub_cpu_is_cpuid_supported (void) +{ + grub_uint32_t id_supported; + + __asm__ ("pushfl\n\t" + "popl %%eax /* Get EFLAGS into EAX */\n\t" + "movl %%eax, %%ecx /* Save original flags in ECX */\n\t" + "xorl $0x200000, %%eax /* Flip ID bit in EFLAGS */\n\t" + "pushl %%eax /* Store modified EFLAGS on stack */\n\t" + "popfl /* Replace current EFLAGS */\n\t" + "pushfl /* Read back the EFLAGS */\n\t" + "popl %%eax /* Get EFLAGS into EAX */\n\t" + "xorl %%ecx, %%eax /* Check if flag could be modified */\n\t" + : "=a" (id_supported) + : /* No inputs. */ + : /* Clobbered: */ "%rcx"); + + return id_supported != 0; +} + +static __inline int +grub_cpu_is_tsc_supported (void) +{ + if (! grub_cpu_is_cpuid_supported ()) + return 0; + + grub_uint32_t features; + __asm__ ("movl $1, %%eax\n\t" + "cpuid" + : "=d" (features) + : /* No inputs. */ + : /* Clobbered: */ "%rax", "%rbx", "%rcx"); + return (features & (1 << 4)) != 0; +} + +void grub_tsc_init (void); +grub_uint64_t grub_tsc_get_time_ms (void); + +#endif /* ! KERNEL_CPU_TSC_HEADER */ === added file 'include/grub/menu.h' --- include/grub/menu.h 1970-01-01 00:00:00 +0000 +++ include/grub/menu.h 2008-08-03 02:14:40 +0000 @@ -0,0 +1,63 @@ +/* menu.h - Menu and menu entry model declarations. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,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_MENU_HEADER +#define GRUB_MENU_HEADER 1 + +struct grub_menu_entry_class +{ + char *name; + struct grub_menu_entry_class *next; +}; + +/* The menu entry. */ +struct grub_menu_entry +{ + /* The title name. */ + const char *title; + + /* The classes associated with the menu entry: + used to choose an icon or other style attributes. + This is a dummy head node for the linked list, so for an entry E, + E.classes->next is the first class if it is not NULL. */ + struct grub_menu_entry_class *classes; + + /* The commands associated with this menu entry. */ + struct grub_script *commands; + + /* The sourcecode of the menu entry, used by the editor. */ + const char *sourcecode; + + /* The next element. */ + struct grub_menu_entry *next; +}; +typedef struct grub_menu_entry *grub_menu_entry_t; + +/* The menu. */ +struct grub_menu +{ + /* The size of a menu. */ + int size; + + /* The list of menu entries. */ + grub_menu_entry_t entry_list; +}; +typedef struct grub_menu *grub_menu_t; + +#endif /* GRUB_MENU_HEADER */ === added file 'include/grub/menu_viewer.h' --- include/grub/menu_viewer.h 1970-01-01 00:00:00 +0000 +++ include/grub/menu_viewer.h 2008-07-19 19:07:47 +0000 @@ -0,0 +1,48 @@ +/* menu_viewer.h - Interface to menu viewer implementations. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,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_MENU_VIEWER_HEADER +#define GRUB_MENU_VIEWER_HEADER 1 + +#include +#include +#include +#include + +struct grub_menu_viewer +{ + /* The menu viewer name. */ + const char *name; + + grub_err_t (*show_menu) (grub_menu_t menu, int nested); + + struct grub_menu_viewer *next; +}; +typedef struct grub_menu_viewer *grub_menu_viewer_t; + +void EXPORT_FUNC(grub_menu_viewer_init) (void); + +void EXPORT_FUNC(grub_menu_viewer_register) (grub_menu_viewer_t viewer); + +grub_err_t EXPORT_FUNC(grub_menu_viewer_show_menu) (grub_menu_t menu, int nested); + +/* Return nonzero iff the menu viewer should clean up and return ASAP. */ +int EXPORT_FUNC(grub_menu_viewer_should_return) (void); + +#endif /* GRUB_MENU_VIEWER_HEADER */ === modified file 'include/grub/normal.h' --- include/grub/normal.h 2008-03-26 12:01:02 +0000 +++ include/grub/normal.h 2008-08-03 04:00:22 +0000 @@ -25,6 +25,7 @@ #include #include #include +#include /* The maximum size of a command-line. */ #define GRUB_MAX_CMDLINE 1600 @@ -39,7 +40,7 @@ #define GRUB_COMMAND_FLAG_TITLE 0x4 /* Don't print the command on booting. */ #define GRUB_COMMAND_FLAG_NO_ECHO 0x8 -/* Don't print the command on booting. */ +/* Pass arguments to the command without parsing options. */ #define GRUB_COMMAND_FLAG_NO_ARG_PARSE 0x10 /* Not loaded yet. Used for auto-loading. */ #define GRUB_COMMAND_FLAG_NOT_LOADED 0x20 @@ -84,34 +85,6 @@ }; typedef struct grub_command *grub_command_t; -/* The menu entry. */ -struct grub_menu_entry -{ - /* The title name. */ - const char *title; - - /* The commands associated with this menu entry. */ - struct grub_script *commands; - - /* The sourcecode of the menu entry, used by the editor. */ - const char *sourcecode; - - /* The next element. */ - struct grub_menu_entry *next; -}; -typedef struct grub_menu_entry *grub_menu_entry_t; - -/* The menu. */ -struct grub_menu -{ - /* The size of a menu. */ - int size; - - /* The list of menu entries. */ - grub_menu_entry_t entry_list; -}; -typedef struct grub_menu *grub_menu_t; - /* This is used to store the names of filesystem modules for auto-loading. */ struct grub_fs_module_list { @@ -123,10 +96,27 @@ /* To exit from the normal mode. */ extern grub_jmp_buf grub_exit_env; +extern struct grub_menu_viewer grub_normal_terminal_menu_viewer; + +typedef struct grub_menu_execute_callback +{ + void (*notify_booting) (void *userdata, grub_menu_entry_t entry); + void (*notify_fallback) (void *userdata, grub_menu_entry_t entry); + void (*notify_failure) (void *userdata); +} +*grub_menu_execute_callback_t; + void grub_enter_normal_mode (const char *config); void grub_normal_execute (const char *config, int nested); -void grub_menu_run (grub_menu_t menu, int nested); +void grub_menu_execute_with_fallback (grub_menu_t menu, + grub_menu_entry_t entry, + grub_menu_execute_callback_t callback, + void *callback_data); void grub_menu_entry_run (grub_menu_entry_t entry); +int grub_menu_get_default_entry_index (grub_menu_t menu); +int grub_menu_get_timeout (void); +void grub_menu_set_timeout (int timeout); +void grub_menu_execute_entry(grub_menu_entry_t entry); void grub_cmdline_run (int nested); int grub_cmdline_get (const char *prompt, char cmdline[], unsigned max_len, int echo_char, int readline); === added file 'include/grub/stringutil.h' --- include/grub/stringutil.h 1970-01-01 00:00:00 +0000 +++ include/grub/stringutil.h 2008-07-25 15:52:57 +0000 @@ -0,0 +1,32 @@ +/* gfxmenu_stringutil.h - String utilities for the graphical menu interface. */ +/* + * 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 GRUB_GFXMENU_STRINGUTIL_HEADER +#define GRUB_GFXMENU_STRINGUTIL_HEADER 1 + +#include + +char *grub_new_substring (const char *buf, + grub_size_t start, grub_size_t end); + +char *grub_resolve_relative_path (const char *base, const char *path); + +char *grub_get_dirname (const char *file_path); + +#endif /* GRUB_GFXMENU_STRINGUTIL_HEADER */ === modified file 'include/grub/time.h' --- include/grub/time.h 2007-10-22 19:02:16 +0000 +++ include/grub/time.h 2008-07-28 16:51:30 +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,15 @@ #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); + +grub_uint64_t grub_rtc_get_time_ms (void); static __inline void grub_sleep (grub_uint32_t s) @@ -32,4 +35,6 @@ grub_millisleep (1000 * s); } +void grub_install_get_time_ms (grub_uint64_t (*get_time_ms_func) (void)); + #endif /* ! KERNEL_TIME_HEADER */ === modified file 'include/grub/video.h' --- include/grub/video.h 2008-01-01 12:02:07 +0000 +++ include/grub/video.h 2008-07-19 19:31:46 +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 @@ -137,6 +151,16 @@ grub_uint8_t a; /* Reserved bits value (0-255). */ }; +/* A 2D rectangle type. */ +struct grub_video_rect +{ + int x; + int y; + int width; + int height; +}; +typedef struct grub_video_rect grub_video_rect_t; + struct grub_video_adapter { /* The video adapter name. */ @@ -183,6 +207,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 +283,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 +301,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, @@ -282,4 +317,21 @@ grub_err_t grub_video_get_active_render_target (struct grub_video_render_target **target); + +/* Defined in video/setmode.c */ + +/* Set the video mode based on the preferred modes specified in MODE_LIST in + the form: x[x][;...] + + For example: 640x480;800x600x8;400x300x32 + + If MODE_LIST is null, or no modes in it are usable, then DEFAULT_WIDTH and + DEFAULT_HEIGHT are used to set the mode. The MODE_FLAGS argument determines + the video mode flags such as double buffering that are used. */ + +grub_err_t +grub_video_setup_preferred_mode (const char *mode_list, int mode_flags, + int default_width, int default_height); + + #endif /* ! GRUB_VIDEO_HEADER */ === added directory 'kern/generic' === added file 'kern/generic/millisleep.c' --- kern/generic/millisleep.c 1970-01-01 00:00:00 +0000 +++ kern/generic/millisleep.c 2008-07-28 16:51:30 +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 (); +} === added file 'kern/generic/rtc_get_time_ms.c' --- kern/generic/rtc_get_time_ms.c 1970-01-01 00:00:00 +0000 +++ kern/generic/rtc_get_time_ms.c 2008-07-28 16:51:30 +0000 @@ -0,0 +1,37 @@ +/* rtc_get_time_ms.c - get_time_ms 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_rtc_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); +} === 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-28 16:23:46 +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_init (); } void === modified file 'kern/i386/linuxbios/init.c' --- kern/i386/linuxbios/init.c 2008-07-31 18:33:23 +0000 +++ kern/i386/linuxbios/init.c 2008-08-03 03:57:46 +0000 @@ -143,6 +143,8 @@ /* This variable indicates size, not offset. */ grub_upper_mem -= GRUB_MEMORY_MACHINE_UPPER_START; + + grub_install_get_time_ms (grub_rtc_get_time_ms); } void === modified file 'kern/i386/pc/init.c' --- kern/i386/pc/init.c 2008-08-02 22:24:34 +0000 +++ kern/i386/pc/init.c 2008-08-03 03:57:46 +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_init (); } void === added file 'kern/i386/tsc.c' --- kern/i386/tsc.c 1970-01-01 00:00:00 +0000 +++ kern/i386/tsc.c 2008-07-28 16:51:30 +0000 @@ -0,0 +1,102 @@ +/* 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; + + +grub_uint64_t +grub_tsc_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. */ +static void +calibrate_tsc (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; +} + +void +grub_tsc_init (void) +{ + if (grub_cpu_is_tsc_supported ()) + { + calibrate_tsc (); + grub_install_get_time_ms (grub_tsc_get_time_ms); + } + else + { + grub_install_get_time_ms (grub_rtc_get_time_ms); + } +} === modified file 'kern/ieee1275/init.c' --- kern/ieee1275/init.c 2008-07-30 09:42:11 +0000 +++ kern/ieee1275/init.c 2008-08-03 03:57:46 +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 (); @@ -209,6 +203,8 @@ #endif +static grub_uint64_t ieee1275_get_time_ms (void); + void grub_machine_init (void) { @@ -258,6 +254,8 @@ } } } + + grub_install_get_time_ms (ieee1275_get_time_ms); } void @@ -267,8 +265,8 @@ grub_console_fini (); } -grub_uint32_t -grub_get_rtc (void) +static grub_uint64_t +ieee1275_get_time_ms (void) { grub_uint32_t msecs = 0; @@ -277,6 +275,12 @@ return msecs; } +grub_uint32_t +grub_get_rtc (void) +{ + return ieee1275_get_time_ms (); +} + grub_addr_t grub_arch_modules_addr (void) { === added file 'kern/menu_viewer.c' --- kern/menu_viewer.c 1970-01-01 00:00:00 +0000 +++ kern/menu_viewer.c 2008-07-19 19:07:47 +0000 @@ -0,0 +1,99 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,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 + +/* The list of menu viewers. */ +static grub_menu_viewer_t menu_viewer_list; + +static int should_return; +static int menu_viewer_changed; + +void +grub_menu_viewer_register (grub_menu_viewer_t viewer) +{ + viewer->next = menu_viewer_list; + menu_viewer_list = viewer; +} + +static grub_menu_viewer_t get_current_menu_viewer (void) +{ + const char *selected_name = grub_env_get ("menuviewer"); + + /* If none selected, pick the last registered one. */ + if (selected_name == 0) + return menu_viewer_list; + + grub_menu_viewer_t cur; + for (cur = menu_viewer_list; cur; cur = cur->next) + { + if (grub_strcmp (cur->name, selected_name) == 0) + return cur; + } + + /* Fall back to the first entry (or null). */ + return menu_viewer_list; +} + +grub_err_t +grub_menu_viewer_show_menu (grub_menu_t menu, int nested) +{ + grub_err_t err; + int repeat = 0; + do + { + repeat = 0; + menu_viewer_changed = 0; + grub_menu_viewer_t cur = get_current_menu_viewer (); + if (!cur) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "No menu viewer available."); + + should_return = 0; + err = cur->show_menu (menu, nested); + if (menu_viewer_changed) + repeat = 1; + } + while (repeat); + return err; +} + +int +grub_menu_viewer_should_return (void) +{ + return should_return; +} + +static char * +menuviewer_write_hook (struct grub_env_var *var __attribute__ ((unused)), + const char *val) +{ + menu_viewer_changed = 1; + should_return = 1; + return grub_strdup (val); +} + +void +grub_menu_viewer_init (void) +{ + grub_register_variable_hook ("menuviewer", 0, menuviewer_write_hook); +} + === 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-28 16:23:46 +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) { @@ -145,6 +139,8 @@ grub_free (prefix); } +grub_uint64_t ieee1275_get_time_ms (void); + void grub_machine_init (void) { @@ -201,6 +197,7 @@ } } + grub_install_get_time_ms (ieee1275_get_time_ms); } void @@ -216,6 +213,12 @@ grub_ieee1275_enter (); } +grub_uint64_t +ieee1275_get_time_ms (void) +{ + return grub_get_rtc (); +} + grub_uint32_t grub_get_rtc (void) { === added file 'kern/time.c' --- kern/time.c 1970-01-01 00:00:00 +0000 +++ kern/time.c 2008-07-28 16:23:46 +0000 @@ -0,0 +1,37 @@ +/* time.c - kernel time 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 + +typedef grub_uint64_t (*get_time_ms_func_t) (void); + +/* Function pointer to the implementation in use. */ +static get_time_ms_func_t get_time_ms_func; + +grub_uint64_t +grub_get_time_ms (void) +{ + return get_time_ms_func (); +} + +void +grub_install_get_time_ms (get_time_ms_func_t func) +{ + get_time_ms_func = func; +} === modified file 'normal/main.c' --- normal/main.c 2008-07-29 14:07:47 +0000 +++ normal/main.c 2008-08-03 04:00:22 +0000 @@ -28,6 +28,7 @@ #include #include #include +#include grub_jmp_buf grub_exit_env; @@ -147,14 +148,132 @@ grub_env_unset_data_slot ("menu"); } +static void +free_menu_entry_classes (struct grub_menu_entry_class *head) +{ + /* Free all the classes. */ + while (head) + { + struct grub_menu_entry_class *next; + + grub_free (head->name); + next = head->next; + grub_free (head); + head = next; + } +} + +/* The tag that can be added to a menu entry's title to specify a class + for the UI to use in selecting an icon or other visual attributes. */ +static const char entry_class_attr_tag[] = "|class="; + +#define ENTRY_ATTR_SEPARATOR_CHAR ',' + +/* Parse and strip a possible "class" attribute in the title. + This code is not designed to support other attributes than "class" + in the title since, we are planning to use a better method of + specifying this information in the future. The parameter TITLE is + modified by storing a '\0' at the appropriate location to strip the + class information, if it exists. The class list is stored into *HEAD. */ +static struct grub_menu_entry_class * +get_classes_from_entry_title (char *title) +{ + struct grub_menu_entry_class *head; + char *attr_start; + + head = grub_malloc (sizeof (struct grub_menu_entry_class)); + if (! head) + return 0; + head->name = 0; + head->next = 0; + + attr_start = grub_strstr (title, entry_class_attr_tag); + if (attr_start) + { + struct grub_menu_entry_class *tail; + const char *p; + + /* Trim the properties off of the title. */ + *attr_start = '\0'; + + /* Move the pointer to the beginning of the first class name. */ + attr_start += grub_strlen (entry_class_attr_tag); + + tail = head; + p = attr_start; + while (p && *p) + { + const char *q; + const char *end; + const char *next_start; + + /* Skip any leading whitespace. */ + while (*p && grub_isspace (*p)) + p++; + + /* Find the comma terminating this one ... */ + q = grub_strchr (p, ENTRY_ATTR_SEPARATOR_CHAR); + /* ... or if it's the last one, find the '\0' terminator. */ + if (q) + { + end = q - 1; + next_start = q + 1; + } + else + { + /* For the last class, extend it to the end. */ + end = p + grub_strlen (p); + next_start = 0; + } + + /* Trim any trailing whitespace. */ + while (end > p && grub_isspace (*end)) + end--; + + grub_size_t len = end - p + 1; + /* Copy the class name into a new string. */ + char *class_name = grub_malloc (len + 1); + if (! class_name) + { + free_menu_entry_classes (head); + return 0; + } + grub_memcpy (class_name, p, len); + class_name[len] = '\0'; + + /* Create a new class and add it at the tail of the list. */ + struct grub_menu_entry_class *new_class; + new_class = grub_malloc (sizeof (struct grub_menu_entry_class)); + if (! new_class) + { + grub_free (class_name); + free_menu_entry_classes (head); + return 0; + } + /* Fill in the new class node. */ + new_class->name = class_name; + new_class->next = 0; + /* Link the tail to it, and make it the new tail. */ + tail->next = new_class; + tail = new_class; + + /* Advance the character pointer. */ + p = next_start; + } + } + + return head; +} + grub_err_t grub_normal_menu_addentry (const char *title, struct grub_script *script, const char *sourcecode) { - const char *menutitle; + char *menutitle; const char *menusourcecode; grub_menu_t menu; grub_menu_entry_t *last; + struct grub_menu_entry_class *classes; menu = grub_env_get_data_slot("menu"); if (! menu) @@ -173,6 +292,14 @@ return grub_errno; } + classes = get_classes_from_entry_title (menutitle); + if (! classes) + { + grub_free ((void *) menutitle); + grub_free ((void *) menusourcecode); + return grub_errno; + } + /* Add the menu entry at the end of the list. */ while (*last) last = &(*last)->next; @@ -180,6 +307,7 @@ *last = grub_malloc (sizeof (**last)); if (! *last) { + free_menu_entry_classes (classes); grub_free ((void *) menutitle); grub_free ((void *) menusourcecode); return grub_errno; @@ -187,6 +315,7 @@ (*last)->commands = script; (*last)->title = menutitle; + (*last)->classes = classes; (*last)->next = 0; (*last)->sourcecode = menusourcecode; @@ -476,7 +605,7 @@ if (menu && menu->size) { - grub_menu_run (menu, nested); + grub_menu_viewer_show_menu (menu, nested); if (nested) free_menu (menu); } @@ -519,6 +648,8 @@ if (mod) grub_dl_ref (mod); + grub_menu_viewer_register (&grub_normal_terminal_menu_viewer); + grub_set_history (GRUB_DEFAULT_HISTORY_SIZE); /* Register a command "normal" for the rescue mode. */ @@ -535,6 +666,8 @@ /* This registers some built-in commands. */ grub_command_init (); + + grub_menu_viewer_init (); } GRUB_MOD_FINI(normal) === modified file 'normal/menu.c' --- normal/menu.c 2008-02-09 11:00:19 +0000 +++ normal/menu.c 2008-07-26 23:37:04 +0000 @@ -21,9 +21,11 @@ #include #include #include +#include #include #include #include +#include static grub_uint8_t grub_color_menu_normal; static grub_uint8_t grub_color_menu_highlight; @@ -241,8 +243,8 @@ /* Return the current timeout. If the variable "timeout" is not set or invalid, return -1. */ -static int -get_timeout (void) +int +grub_menu_get_timeout (void) { char *val; int timeout; @@ -269,8 +271,8 @@ } /* Set current timeout in the variable "timeout". */ -static void -set_timeout (int timeout) +void +grub_menu_set_timeout (int timeout) { /* Ignore TIMEOUT if it is zero, because it will be unset really soon. */ if (timeout > 0) @@ -308,6 +310,57 @@ return entry; } +/* Get the default menu entry index. */ +int +grub_menu_get_default_entry_index (grub_menu_t menu) +{ + int i = get_entry_number ("default"); + + /* If DEFAULT_ENTRY is not within the menu entries, fall back to + the first entry. */ + if (i < 0 || i >= menu->size) + i = 0; + + return i; +} + +/* Get the first entry number from the variable NAME, which is a + space-separated list of nonnegative integers. The entry number which + is returned is stripped from the value of NAME. */ +static int +get_and_remove_first_entry_number (const char *name) +{ + char *val; + char *tail; + int entry; + + val = grub_env_get (name); + if (! val) + return -1; + + grub_error_push (); + + entry = (int) grub_strtoul (val, &tail, 0); + + if (grub_errno == GRUB_ERR_NONE) + { + /* Skip whitespace to find the next digit. */ + while (*tail && grub_isspace (*tail)) + tail++; + grub_env_set (name, tail); + } + else + { + grub_env_unset (name); + grub_errno = GRUB_ERR_NONE; + entry = -1; + } + + grub_error_pop (); + + return entry; +} + static void print_timeout (int timeout, int offset, int second_stage) { @@ -332,15 +385,10 @@ first = 0; - default_entry = get_entry_number ("default"); - - /* If DEFAULT_ENTRY is not within the menu entries, fall back to - the first entry. */ - if (default_entry < 0 || default_entry >= menu->size) - default_entry = 0; + default_entry = grub_menu_get_default_entry_index (menu); /* If timeout is 0, drawing is pointless (and ugly). */ - if (get_timeout () == 0) + if (grub_menu_get_timeout () == 0) return default_entry; offset = default_entry; @@ -359,15 +407,15 @@ print_entries (menu, first, offset); grub_refresh (); - timeout = get_timeout (); + timeout = grub_menu_get_timeout (); if (timeout > 0) print_timeout (timeout, offset, 0); - while (1) + while (!grub_menu_viewer_should_return ()) { int c; - timeout = get_timeout (); + timeout = grub_menu_get_timeout (); if (timeout > 0) { @@ -377,7 +425,7 @@ if (current_time - saved_time >= GRUB_TICKS_PER_SECOND) { timeout--; - set_timeout (timeout); + grub_menu_set_timeout (timeout); saved_time = current_time; print_timeout (timeout, offset, 1); } @@ -468,6 +516,10 @@ } goto refresh; + case 't': + grub_env_set ("menuviewer", "protomenu"); + goto refresh; + default: break; } @@ -476,13 +528,14 @@ } } - /* Never reach here. */ + /* Exit menu without activating an item. This occurs if the user presses + * 't', switching to the graphical menu viewer. */ return -1; } /* Run a menu entry. */ -static void -run_menu_entry (grub_menu_entry_t entry) +void +grub_menu_execute_entry(grub_menu_entry_t entry) { grub_script_execute (entry->commands); @@ -491,15 +544,80 @@ grub_command_execute ("boot", 0); } +/* Execute ENTRY from the menu MENU, falling back to entries specified + in the environment variable "fallback" if it fails. CALLBACK is a + pointer to a struct of function pointers which are used to allow the + caller provide feedback to the user. */ void -grub_menu_run (grub_menu_t menu, int nested) +grub_menu_execute_with_fallback (grub_menu_t menu, + grub_menu_entry_t entry, + grub_menu_execute_callback_t callback, + void *callback_data) +{ + int fallback_entry; + + callback->notify_booting (callback_data, entry); + + grub_menu_execute_entry (entry); + + /* Deal with fallback entries. */ + while ((fallback_entry = get_and_remove_first_entry_number ("fallback")) + >= 0) + { + grub_print_error (); + grub_errno = GRUB_ERR_NONE; + + entry = get_entry (menu, fallback_entry); + callback->notify_fallback (callback_data, entry); + grub_menu_execute_entry (entry); + } + + if (grub_errno != GRUB_ERR_NONE) + callback->notify_failure (callback_data); +} + +static void +notify_booting (void *userdata __attribute__((unused)), + grub_menu_entry_t entry) +{ + grub_printf (" Booting \'%s\'\n\n", entry->title); +} + +static void +notify_fallback (void *userdata __attribute__((unused)), + grub_menu_entry_t entry) +{ + grub_printf ("\n Falling back to \'%s\'\n\n", entry->title); + grub_millisleep (2000); +} + +static void +notify_execution_failure (void *userdata __attribute__((unused))) +{ + if (grub_errno != GRUB_ERR_NONE) + { + grub_print_error (); + grub_errno = GRUB_ERR_NONE; + + grub_wait_after_message (); + } +} + +static struct grub_menu_execute_callback execution_callback = +{ + .notify_booting = notify_booting, + .notify_fallback = notify_fallback, + .notify_failure = notify_execution_failure +}; + +static grub_err_t +show_menu (grub_menu_t menu, int nested) { while (1) { int boot_entry; grub_menu_entry_t e; - int fallback_entry; - + boot_entry = run_menu (menu, nested); if (boot_entry < 0) break; @@ -507,34 +625,18 @@ e = get_entry (menu, boot_entry); if (! e) continue; /* Menu is empty. */ - + grub_cls (); grub_setcursor (1); - grub_printf (" Booting \'%s\'\n\n", e->title); - - run_menu_entry (e); - - /* Deal with a fallback entry. */ - /* FIXME: Multiple fallback entries like GRUB Legacy. */ - fallback_entry = get_entry_number ("fallback"); - if (fallback_entry >= 0) - { - grub_print_error (); - grub_errno = GRUB_ERR_NONE; - - e = get_entry (menu, fallback_entry); - grub_env_unset ("fallback"); - grub_printf ("\n Falling back to \'%s\'\n\n", e->title); - run_menu_entry (e); - } - - if (grub_errno != GRUB_ERR_NONE) - { - grub_print_error (); - grub_errno = GRUB_ERR_NONE; - - grub_wait_after_message (); - } + grub_menu_execute_with_fallback (menu, e, &execution_callback, 0); } + + return GRUB_ERR_NONE; } + +struct grub_menu_viewer grub_normal_terminal_menu_viewer = +{ + .name = "terminal", + .show_menu = show_menu +}; === modified file 'term/gfxterm.c' --- term/gfxterm.c 2008-01-01 12:02:07 +0000 +++ term/gfxterm.c 2008-07-19 19:56:06 +0000 @@ -28,14 +28,12 @@ #include #include #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 @@ -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; @@ -107,10 +108,20 @@ struct grub_colored_char *text_buffer; }; +static int refcount; +static struct grub_video_render_target *render_target; +static grub_video_rect_t window; static struct grub_virtual_screen virtual_screen; +static grub_gfxterm_repaint_callback_t repaint_callback; + +static grub_err_t init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width); + +static void destroy_window (void); + static grub_dl_t my_mod; -static struct grub_video_mode_info mode_info; static struct grub_video_render_target *text_layer; @@ -170,18 +181,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; @@ -227,281 +245,129 @@ } static grub_err_t +init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width) +{ + /* Clean up any prior instance. */ + destroy_window (); + + /* Create virtual screen. */ + if (grub_virtual_screen_setup (border_width, border_width, + width - 2 * border_width, + height - 2 * border_width, + font_name) + != GRUB_ERR_NONE) + { + return grub_errno; + } + + /* Set the render target. */ + render_target = target; + + /* Set window bounds. */ + window.x = x; + window.y = y; + window.width = width; + window.height = height; + + /* Mark whole window as dirty. */ + dirty_region_reset (); + dirty_region_add (0, 0, width, height); + + return (grub_errno = GRUB_ERR_NONE); +} + +grub_err_t +grub_gfxterm_init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width) +{ + grub_errno = GRUB_ERR_NONE; + if (refcount++ == 0) + init_window (target, x, y, width, height, font_name, border_width); + return grub_errno; +} + +static grub_err_t grub_gfxterm_init (void) { - char *modevar; - int width = DEFAULT_VIDEO_WIDTH; - int height = DEFAULT_VIDEO_HEIGHT; - int depth = -1; - int flags = DEFAULT_VIDEO_FLAGS; - grub_video_color_t color; + /* If gfxterm has already been initialized by calling the init_window + function, then leave it alone when it is set as the current terminal. */ + if (refcount++ != 0) + return GRUB_ERR_NONE; /* Parse gfxmode environment variable if set. */ - modevar = grub_env_get ("gfxmode"); - if (modevar) - { - char *tmp; - char *next_mode; - char *current_mode; - char *param; - char *value; - int mode_found = 0; - - /* Take copy of env.var. as we don't want to modify that. */ - tmp = grub_strdup (modevar); - modevar = tmp; - - if (grub_errno != GRUB_ERR_NONE) - return grub_errno; - - /* Initialize next mode. */ - next_mode = modevar; - - /* Loop until all modes has been tested out. */ - while (next_mode != NULL) - { - /* Use last next_mode as current mode. */ - tmp = next_mode; - - /* Reset video mode settings. */ - width = DEFAULT_VIDEO_WIDTH; - height = DEFAULT_VIDEO_HEIGHT; - depth = -1; - flags = DEFAULT_VIDEO_FLAGS; - - /* Save position of next mode and separate modes. */ - next_mode = grub_strchr(next_mode, ';'); - if (next_mode) - { - *next_mode = 0; - next_mode++; - } - - /* Skip whitespace. */ - while (grub_isspace (*tmp)) - tmp++; - - /* Initialize token holders. */ - current_mode = tmp; - param = tmp; - value = NULL; - - /* Parse x[x]*/ - - /* Find width value. */ - value = param; - param = grub_strchr(param, 'x'); - if (param == NULL) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - *param = 0; - param++; - - width = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - /* Find height value. */ - value = param; - param = grub_strchr(param, 'x'); - if (param == NULL) - { - height = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - } - else - { - /* We have optional color depth value. */ - *param = 0; - param++; - - height = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - /* Convert color depth value. */ - value = param; - depth = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - } - - /* Try out video mode. */ - - /* If we have 8 or less bits, then assume that it is indexed color mode. */ - if ((depth <= 8) && (depth != -1)) - flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; - - /* We have more than 8 bits, then assume that it is RGB color mode. */ - if (depth > 8) - flags |= GRUB_VIDEO_MODE_TYPE_RGB; - - /* If user requested specific depth, forward that information to driver. */ - if (depth != -1) - flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) - & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; - - /* Try to initialize requested mode. Ignore any errors. */ - grub_error_push (); - if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) - { - grub_error_pop (); - continue; - } - - /* Figure out what mode we ended up. */ - if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) - { - /* Couldn't get video mode info, restore old mode and continue to next one. */ - grub_error_pop (); - - grub_video_restore (); - continue; - } - - /* Restore state of error stack. */ - grub_error_pop (); - - /* Mode found! Exit loop. */ - mode_found = 1; - break; - } - - /* Free memory. */ - grub_free (modevar); - - if (!mode_found) - return grub_error (GRUB_ERR_BAD_ARGUMENT, - "No suitable mode found."); - } - else - { - /* No gfxmode variable set, use defaults. */ - - /* If we have 8 or less bits, then assume that it is indexed color mode. */ - if ((depth <= 8) && (depth != -1)) - flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; - - /* We have more than 8 bits, then assume that it is RGB color mode. */ - if (depth > 8) - flags |= GRUB_VIDEO_MODE_TYPE_RGB; - - /* If user requested specific depth, forward that information to driver. */ - if (depth != -1) - flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) - & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; - - /* Initialize user requested mode. */ - if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) - return grub_errno; - - /* Figure out what mode we ended up. */ - if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) - { - grub_video_restore (); - return grub_errno; - } + const char *modevar = grub_env_get ("gfxmode"); + if (grub_video_setup_preferred_mode (modevar, 0, + DEFAULT_VIDEO_WIDTH, + DEFAULT_VIDEO_HEIGHT) + != GRUB_ERR_NONE) + return grub_errno; + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + { + grub_video_restore (); + return grub_errno; } /* Make sure screen is black. */ - color = grub_video_map_rgb (0, 0, 0); - grub_video_fill_rect (color, 0, 0, mode_info.width, mode_info.height); + grub_video_fill_rect (grub_video_map_rgb (0, 0, 0), + 0, 0, mode_info.width, mode_info.height); bitmap = 0; + /* Select the font to use. */ + char *font_name = grub_env_get ("gfxterm_font"); + if (!font_name) + font_name = ""; /* Allow fallback to any font. */ + /* Leave borders for virtual screen. */ - width = mode_info.width - (2 * DEFAULT_BORDER_WIDTH); - height = mode_info.height - (2 * DEFAULT_BORDER_WIDTH); - - /* Create virtual screen. */ - if (grub_virtual_screen_setup (DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_WIDTH, - width, height) != GRUB_ERR_NONE) + if (init_window (GRUB_VIDEO_RENDER_TARGET_DISPLAY, + 0, 0, mode_info.width, mode_info.height, + font_name, + DEFAULT_BORDER_WIDTH) != GRUB_ERR_NONE) { grub_video_restore (); return grub_errno; } - /* Mark whole screen as dirty. */ - dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); - return (grub_errno = GRUB_ERR_NONE); } +static void +destroy_window (void) +{ + if (bitmap) + { + grub_video_bitmap_destroy (bitmap); + bitmap = 0; + } + + repaint_callback = 0; + grub_virtual_screen_free (); +} + +void +grub_gfxterm_destroy_window (void) +{ + if (--refcount == 0) + destroy_window (); +} + static grub_err_t grub_gfxterm_fini (void) { - if (bitmap) + /* Don't destroy an explicitly initialized terminal instance when it is + unset as the current terminal. */ + if (--refcount == 0) { - grub_video_bitmap_destroy (bitmap); - bitmap = 0; + destroy_window (); + grub_video_restore (); } - grub_virtual_screen_free (); - - grub_video_restore (); - - return GRUB_ERR_NONE; + return (grub_errno = GRUB_ERR_NONE); } static void @@ -509,9 +375,15 @@ unsigned int width, unsigned int height) { grub_video_color_t color; - - grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); - + grub_video_rect_t saved_view; + + grub_video_set_active_render_target (render_target); + /* Save viewport and set it to our window. */ + grub_video_get_viewport ((unsigned *) &saved_view.x, + (unsigned *) &saved_view.y, + (unsigned *) &saved_view.width, + (unsigned *) &saved_view.height); + grub_video_set_viewport (window.x, window.y, window.width, window.height); if (bitmap) { @@ -578,6 +450,14 @@ y - virtual_screen.offset_y, width, height); } + + /* Restore saved viewport. */ + grub_video_set_viewport (saved_view.x, saved_view.y, + saved_view.width, saved_view.height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + if (repaint_callback) + repaint_callback (x, y, width, height); } static void @@ -661,11 +541,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 +556,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 +567,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 +587,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; @@ -769,7 +652,16 @@ dirty_region_add_virtualscreen (); } else - { + { + grub_video_rect_t saved_view; + grub_video_set_active_render_target (render_target); + /* Save viewport and set it to our window. */ + grub_video_get_viewport ((unsigned *) &saved_view.x, + (unsigned *) &saved_view.y, + (unsigned *) &saved_view.width, + (unsigned *) &saved_view.height); + grub_video_set_viewport (window.x, window.y, window.width, window.height); + /* Clear new border area. */ grub_video_fill_rect (color, virtual_screen.offset_x, virtual_screen.offset_y, @@ -778,10 +670,18 @@ /* Scroll physical screen. */ grub_video_scroll (color, 0, -virtual_screen.char_height); + /* Restore saved viewport. */ + grub_video_set_viewport (saved_view.x, saved_view.y, + saved_view.width, saved_view.height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + /* Draw cursor if visible. */ if (virtual_screen.cursor_state) write_cursor (); } + + if (repaint_callback) + repaint_callback (window.x, window.y, window.width, window.height); } static void @@ -822,14 +722,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 +743,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 +763,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; @@ -877,13 +781,17 @@ } static grub_ssize_t -grub_gfxterm_getcharwidth (grub_uint32_t c) +grub_gfxterm_getcharwidth (grub_uint32_t c __attribute__((unused))) { - 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 @@ -945,7 +853,8 @@ /* Clear text layer. */ grub_video_set_active_render_target (text_layer); color = virtual_screen.bg_color; - grub_video_fill_rect (color, 0, 0, mode_info.width, mode_info.height); + grub_video_fill_rect (color, 0, 0, + virtual_screen.width, virtual_screen.height); grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); /* Mark virtual screen to be redrawn. */ @@ -1014,8 +923,23 @@ dirty_region_redraw (); } +void +grub_gfxterm_set_repaint_callback (grub_gfxterm_repaint_callback_t func) +{ + repaint_callback = func; +} + +/* 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) { @@ -1031,7 +955,7 @@ /* Mark whole screen as dirty. */ dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); + dirty_region_add (0, 0, window.width, window.height); } /* If filename was provided, try to load that. */ @@ -1042,16 +966,40 @@ 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 (window.width != (int) grub_video_bitmap_get_width (bitmap) + || window.height != (int) grub_video_bitmap_get_height (bitmap)) + { + struct grub_video_bitmap *scaled_bitmap; + grub_video_bitmap_create_scaled (&scaled_bitmap, + window.width, + window.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 (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); + dirty_region_add (0, 0, window.width, window.height); } } @@ -1082,9 +1030,16 @@ .next = 0 }; +grub_term_t +grub_gfxterm_get_term (void) +{ + return &grub_video_term; +} + GRUB_MOD_INIT(term_gfxterm) { my_mod = mod; + refcount = 0; grub_term_register (&grub_video_term); grub_register_command ("background_image", @@ -1092,7 +1047,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 'util/i386/pc/grub-mkrescue.in' --- util/i386/pc/grub-mkrescue.in 2008-07-12 14:40:50 +0000 +++ util/i386/pc/grub-mkrescue.in 2008-07-19 21:39:24 +0000 @@ -71,7 +71,7 @@ --modules=*) modules=`echo "$option" | sed 's/--modules=//'` ;; --overlay=*) - overlay=`echo "$option" | sed 's/--overlay=//'` ;; + overlay=${overlay}${overlay:+ }`echo "$option" | sed 's/--overlay=//'` ;; --pkglibdir=*) input_dir=`echo "$option" | sed 's/--pkglibdir=//'` ;; --grub-mkimage=*) @@ -124,9 +124,10 @@ echo "insmod $i" done > ${aux_dir}/boot/grub/grub.cfg -if test "x$overlay" = x ; then : ; else - cp -dpR ${overlay}/* ${aux_dir}/ -fi +for d in ${overlay}; do + echo "Overlaying $d" + cp -vdpR ${d}/* ${aux_dir}/ +done if [ "x${image_type}" = xfloppy -o "x${emulation}" = xfloppy ] ; then # build memdisk === 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-08-03 01:30:02 +0000 @@ -0,0 +1,101 @@ +/* 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) +{ + *dst = 0; + + /* 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); + *dst = 0; + 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-19 19:42:37 +0000 @@ -63,6 +63,7 @@ static struct { struct grub_video_render_target render_target; + int is_double_buffered; /* Is the video mode double buffered? */ unsigned int bytes_per_scan_line; unsigned int bytes_per_pixel; @@ -77,6 +78,24 @@ static grub_uint32_t mode_in_use = 0x55aa; static grub_uint16_t *mode_list; +static struct +{ + grub_size_t page_size; /* The size of a page in bytes. */ + + /* For page flipping strategy. */ + int displayed_page; /* The page # that is the front buffer. */ + int render_page; /* The page # that is the back buffer. */ + + /* For blit strategy. */ + grub_uint8_t *offscreen_buffer; + + /* Virtual functions. */ + int (*update_screen) (void); + int (*destroy) (void); +} doublebuf_state; + +static void double_buffering_init (void); + static void * real2pm (grub_vbe_farptr_t ptr) { @@ -376,6 +395,7 @@ /* Reset frame buffer and render target variables. */ grub_memset (&framebuffer, 0, sizeof(framebuffer)); render_target = &framebuffer.render_target; + grub_memset (&doublebuf_state, 0, sizeof(doublebuf_state)); return GRUB_ERR_NONE; } @@ -391,6 +411,9 @@ /* TODO: Decide, is this something we want to do. */ return grub_errno; + if (doublebuf_state.destroy) + doublebuf_state.destroy(); + /* TODO: Free any resources allocated by driver. */ grub_free (mode_list); mode_list = 0; @@ -533,9 +556,13 @@ 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. */ + /* Mark framebuffer memory as non allocated. */ render_target->is_allocated = 0; - render_target->data = framebuffer.ptr; + + /* Set up double buffering information. */ + framebuffer.is_double_buffered = + ((mode_type & GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED) != 0); + double_buffering_init (); /* Copy default palette to initialize emulated palette. */ for (i = 0; @@ -556,6 +583,166 @@ return grub_error (GRUB_ERR_UNKNOWN_DEVICE, "no matching mode found."); } +/* + Set framebuffer render target page and display the proper page, based on + `doublebuf_state.render_page' and `doublebuf_state.displayed_page', + respectively. + + Returns 0 upon success, nonzero upon failure. + */ +static int +doublebuf_pageflipping_commit (void) +{ + /* Set the render target's data pointer to the start of the render_page. */ + framebuffer.render_target.data = + ((char *) framebuffer.ptr) + + doublebuf_state.page_size * doublebuf_state.render_page; + + /* Tell the video adapter to display the new front page. */ + int display_start_line = + framebuffer.render_target.mode_info.height + * doublebuf_state.displayed_page; + + grub_vbe_status_t vbe_err = + grub_vbe_bios_set_display_start (0, display_start_line); + if (vbe_err != GRUB_VBE_STATUS_OK) + return 1; + + return 0; +} + +static int +doublebuf_pageflipping_update_screen (void) +{ + /* Swap the page numbers in the framebuffer struct. */ + int new_displayed_page = doublebuf_state.render_page; + doublebuf_state.render_page = doublebuf_state.displayed_page; + doublebuf_state.displayed_page = new_displayed_page; + + return doublebuf_pageflipping_commit (); +} + +static int +doublebuf_pageflipping_destroy (void) +{ + doublebuf_state.update_screen = 0; + doublebuf_state.destroy = 0; + return 0; +} + +static int +doublebuf_pageflipping_init (void) +{ + doublebuf_state.page_size = + framebuffer.bytes_per_scan_line * render_target->mode_info.height; + + /* Get video RAM size in bytes. */ + grub_size_t vram_size = controller_info.total_memory << 16; + + if (2 * doublebuf_state.page_size > vram_size) + return 1; /* Not enough video memory for 2 pages. */ + + doublebuf_state.displayed_page = 0; + doublebuf_state.render_page = 1; + + doublebuf_state.update_screen = doublebuf_pageflipping_update_screen; + doublebuf_state.destroy = doublebuf_pageflipping_destroy; + + /* Set the framebuffer memory data pointer and display the right page. */ + if (doublebuf_pageflipping_commit () != GRUB_ERR_NONE) + return 1; /* Unable to set the display start. */ + + framebuffer.render_target.mode_info.mode_type + |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + return 0; +} + +static int +doublebuf_blit_update_screen (void) +{ + grub_memcpy (framebuffer.ptr, + doublebuf_state.offscreen_buffer, + doublebuf_state.page_size); + return 0; +} + +static int +doublebuf_blit_destroy (void) +{ + grub_free (doublebuf_state.offscreen_buffer); + doublebuf_state.offscreen_buffer = 0; + + doublebuf_state.update_screen = 0; + doublebuf_state.destroy = 0; + return 0; +} + +static int +doublebuf_blit_init (void) +{ + doublebuf_state.page_size = + framebuffer.bytes_per_scan_line * render_target->mode_info.height; + + doublebuf_state.offscreen_buffer = (grub_uint8_t *) + grub_malloc (doublebuf_state.page_size); + if (doublebuf_state.offscreen_buffer == 0) + return 1; /* Error. */ + + framebuffer.render_target.data = doublebuf_state.offscreen_buffer; + doublebuf_state.update_screen = doublebuf_blit_update_screen; + doublebuf_state.destroy = doublebuf_blit_destroy; + + framebuffer.render_target.mode_info.mode_type + |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + return 0; +} + +static int +doublebuf_null_update_screen (void) +{ + return 0; +} + +static int +doublebuf_null_destroy (void) +{ + doublebuf_state.update_screen = 0; + doublebuf_state.destroy = 0; + return 0; +} + +static int +doublebuf_null_init (void) +{ + framebuffer.render_target.data = framebuffer.ptr; + doublebuf_state.update_screen = doublebuf_null_update_screen; + doublebuf_state.destroy = doublebuf_null_destroy; + + framebuffer.render_target.mode_info.mode_type + &= ~GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + return 0; +} + +/* Select the best double buffering mode available. */ +static void +double_buffering_init (void) +{ + if (doublebuf_state.destroy) + doublebuf_state.destroy(); + + if (framebuffer.is_double_buffered) + { + if (doublebuf_pageflipping_init () == 0) + return; + + if (doublebuf_blit_init () == 0) + return; + } + + /* Fall back to no double buffering. */ + doublebuf_null_init (); +} + static grub_err_t grub_video_vbe_get_info (struct grub_video_mode_info *mode_info) { @@ -708,6 +895,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 +934,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 +1005,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 +1102,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 +1136,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 +1148,35 @@ 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) - { - grub_video_i386_vbeblit_R8G8B8_R8G8B8X8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGR_888) + { + grub_video_i386_vbeblit_BGR888_RGBX8888 (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_RGBX8888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1005,26 +1184,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 +1227,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 +1263,35 @@ 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) - { - grub_video_i386_vbeblit_R8G8B8A8_R8G8B8A8 (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_R8G8B8A8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_BGRA8888_RGBA8888 (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_RGBA8888 (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_RGBA8888 (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_RGBA8888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1077,26 +1299,42 @@ 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) - { - 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; @@ -1104,7 +1342,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 +1457,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 +1712,13 @@ return GRUB_ERR_NONE; } -static grub_err_t +static grub_err_t grub_video_vbe_swap_buffers (void) { - /* TODO: Implement buffer swapping. */ + if (doublebuf_state.update_screen () != 0) + return grub_error (GRUB_ERR_INVALID_COMMAND, + "Double buffer update failed"); + return GRUB_ERR_NONE; } @@ -1505,17 +1817,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 +1859,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-09 17:27:53 +0000 @@ -35,7 +35,343 @@ #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_BGRA8888_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) +{ + grub_uint32_t *srcptr; + grub_uint32_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + int i; + int j; + + /* 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_uint32_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint32_t *) get_data_ptr (dst, x, y); + + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint32_t color; + unsigned int sr; + unsigned int sg; + unsigned int sb; + unsigned int a; + unsigned int dr; + unsigned int dg; + unsigned int db; + + color = *srcptr++; + + a = color >> 24; + + if (a == 0) + { + /* Skip transparent source pixels. */ + dstptr++; + continue; + } + + sr = (color >> 0) & 0xFF; + sg = (color >> 8) & 0xFF; + sb = (color >> 16) & 0xFF; + + if (a == 255) + { + /* Opaque pixel shortcut. */ + dr = sr; + dg = sg; + db = sb; + } + else + { + /* General pixel color blending. */ + color = *dstptr; + + dr = (color >> 16) & 0xFF; + dr = (dr * (255 - a) + sr * a) / 255; + dg = (color >> 8) & 0xFF; + dg = (dg * (255 - a) + sg * a) / 255; + db = (color >> 0) & 0xFF; + db = (db * (255 - a) + sb * a) / 255; + } + + color = (a << 24) | (dr << 16) | (dg << 8) | db; + + *dstptr++ = color; + } + + srcptr = (grub_uint32_t *) (((grub_uint8_t *) srcptr) + srcrowskip); + dstptr = (grub_uint32_t *) (((grub_uint8_t *) dstptr) + dstrowskip); + } +} + +void +grub_video_i386_vbeblit_BGR888_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) +{ + grub_uint32_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + int i; + int j; + + /* 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_uint32_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_uint32_t color; + unsigned int sr; + unsigned int sg; + unsigned int sb; + unsigned int a; + unsigned int dr; + unsigned int dg; + unsigned int db; + + color = *srcptr++; + + a = color >> 24; + + if (a == 0) + { + /* Skip transparent source pixels. */ + dstptr += 3; + continue; + } + + sr = (color >> 0) & 0xFF; + sg = (color >> 8) & 0xFF; + sb = (color >> 16) & 0xFF; + + if (a == 255) + { + /* Opaque pixel shortcut. */ + dr = sr; + dg = sg; + db = sb; + } + else + { + /* General pixel color blending. */ + color = *dstptr; + + db = dstptr[0]; + db = (db * (255 - a) + sb * a) / 255; + dg = dstptr[1]; + dg = (dg * (255 - a) + sg * a) / 255; + dr = dstptr[2]; + dr = (dr * (255 - a) + sr * a) / 255; + } + + *dstptr++ = db; + *dstptr++ = dg; + *dstptr++ = dr; + } + + srcptr = (grub_uint32_t *) (((grub_uint8_t *) srcptr) + srcrowskip); + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_BGR888_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) +{ + grub_uint32_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + int i; + int j; + + /* 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_uint32_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_uint32_t color; + grub_uint8_t sr; + grub_uint8_t sg; + grub_uint8_t sb; + + color = *srcptr++; + + sr = (color >> 0) & 0xFF; + sg = (color >> 8) & 0xFF; + sb = (color >> 16) & 0xFF; + + *dstptr++ = sb; + *dstptr++ = sg; + *dstptr++ = sr; + } + + srcptr = (grub_uint32_t *) (((grub_uint8_t *) 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 +437,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 +462,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 +529,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 +567,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 +631,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 +668,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 +704,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 +729,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-08-01 03:06:55 +0000 +++ video/readers/jpeg.c 2008-08-03 03:57:46 +0000 @@ -1,20 +1,20 @@ /* - * 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 . - */ +* 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 @@ -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-08-01 03:06:55 +0000 +++ video/readers/png.c 2008-08-03 04:00:22 +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 2008-08-01 03:06:55 +0000 +++ video/readers/tga.c 2008-08-03 03:57:46 +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); === added file 'video/setmode.c' --- video/setmode.c 1970-01-01 00:00:00 +0000 +++ video/setmode.c 2008-07-19 19:31:46 +0000 @@ -0,0 +1,249 @@ +/* video/setmode.c - Smart video mode selection based on preferences. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 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 + +/* Set the video mode based on the preferred modes specified in MODE_LIST in + the form: x[x][;...] + + For example: 640x480;800x600x8;400x300x32 + + If MODE_LIST is null, or no modes in it are usable, then DEFAULT_WIDTH and + DEFAULT_HEIGHT are used to set the mode. The MODE_FLAGS argument determines + the video mode flags such as double buffering that are used. */ + +grub_err_t +grub_video_setup_preferred_mode (const char *mode_list, int mode_flags, + int default_width, int default_height) +{ + int mode_found = 0; + + if (mode_list != NULL) + { + /* Take copy of mode_list as we don't want tat. */ + char *const modes_copy = grub_strdup (mode_list); + if (modes_copy == NULL) + return grub_errno; + + /* Initialize next mode. */ + char *next_mode = modes_copy; + + /* Loop until all modes has been tested out. */ + while ((next_mode != NULL) && !mode_found) + { + /* Use last next_mode as current mode. */ + char *tmp = next_mode; + + int width = -1; + int height = -1; + int depth = -1; + + /* Save position of next mode and separate modes. */ + next_mode = grub_strchr(next_mode, ';'); + if (next_mode) + { + *next_mode = 0; + next_mode++; + } + + /* Skip whitespace. */ + while (grub_isspace (*tmp)) + tmp++; + + /* Initialize token holders. */ + char *current_mode = tmp; + char *param = tmp; + char *value = NULL; + + /* Parse x[x]*/ + + /* Find width value. */ + value = param; + param = grub_strchr(param, 'x'); + if (param == NULL) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + *param = 0; + param++; + + width = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + /* Find height value. */ + value = param; + param = grub_strchr(param, 'x'); + if (param == NULL) + { + height = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + } + else + { + /* We have optional color depth value. */ + *param = 0; + param++; + + height = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + /* Convert color depth value. */ + value = param; + depth = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + } + + /* Try out video mode. */ + + int flags = mode_flags; + /* If we have <= 8 bits, assume it is an indexed color mode. */ + if ((depth <= 8) && (depth != -1)) + flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; + + /* We have > 8 bits; assume that it is RGB color mode. */ + if (depth > 8) + flags |= GRUB_VIDEO_MODE_TYPE_RGB; + + /* If user requested specific depth, pass the request to driver. */ + if (depth != -1) + flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) + & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; + + /* Try to initialize requested mode. Ignore any errors. */ + grub_error_push (); + if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) + { + grub_error_pop (); + continue; + } + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + { + /* Couldn't get video mode info, restore old mode + and continue to next one. */ + grub_error_pop (); + + grub_video_restore (); + continue; + } + + /* Restore state of error stack. */ + grub_error_pop (); + + /* Mode found! Exit loop. */ + mode_found = 1; + } + + /* Free memory. */ + grub_free (modes_copy); + } + + if (!mode_found) + { + /* No gfxmode variable set, or no listed mode was supported. + Use the caller-specified defaults. */ + int flags = mode_flags | GRUB_VIDEO_MODE_TYPE_RGB; + + /* Initialize user requested mode. */ + if (grub_video_setup (default_width, default_height, flags) + != GRUB_ERR_NONE) + return grub_errno; + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + grub_video_restore (); + else + mode_found = 1; + } + + if (!mode_found) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "No suitable mode found."); + + return (grub_errno = GRUB_ERR_NONE); +} === modified file 'video/video.c' --- video/video.c 2008-01-01 12:02:07 +0000 +++ video/video.c 2008-07-09 16:50:11 +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; } } @@ -168,13 +175,22 @@ if (mode_info->bpp == 24) { 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_BGR_888; + } + 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)) { - return GRUB_VIDEO_BLIT_FORMAT_R8G8B8; + return GRUB_VIDEO_BLIT_FORMAT_RGB_888; } } @@ -305,6 +321,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 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 x7elnz41dYxKpPbJtdN2jjpuuya4ZsWCd9t3DG2jB9vhm5bPRsbNWTVuYPVUplNFV3zEzelmbQx0 H4qqvAemT19WLpWWa5ItilF6ngLbOKVDNq6M3jYPzjWMhal5MTIocQc28zKzmKSNTEumJ9dsjdos 2fWB0dJgqsyf78m3fDT7XK1zntN1WGE9ZPFTZz1SmS7xR07dMllnLMxQNCxYiVIlty7Kqrg2CuFV Uya5BcFq6DOzVI721uqzUZUbtFFUvbtnOHTdo0Pls28MKqOacy3zeujtqzcuXDFmu6b9l2qqm9L+ mrhb2K15e2LF0YPTdk9uW7Rdy8bu0ozMTLaRgkBgYwGKjjAc9Zsz4ucuGxkyl2cnKcN5blgQWx3d hABlscDcj7VczQf7jBQgJW/jNbc770hO3hs1TrBc9PHTZgyYr5qnLtV21PF23RknavdjRymyjRq9 J46UeOXb0wbPHbaijQ2YuRuwPwOkD7SCfD29r2PcNt83tdI+4aavrTCOU4DsU0otJFzSvx1visI+ gQE8gLHHF8urHgAR4Cw/x+UFJJSSdP4o+9Vvtym/rTtkyysmz7/dPbw7dtHGJuwSkzvlg+XyyfLB RR03MXjBg6fdcUfK7JLtnzJP7GsJCOaCP1/WPefKlG7dm9Pu8Z6a+DNsuus1YNWDEs+jSJ6ZF1Gb dsbtVGC7dismBw1bPn7fDlw5dM398nLdgzcuHTlLPGDxmeFhy88zZoXSjpNHhmsxUVartmCrdwqu MFlmSjI338XdKOHJyyy+nSUXYtnbVyo3WZOV1HSpy0M2qrxqxbt2jdR2Xf2yJrw5Xd9+ZqZaFGTJ ms4VYrmKjpZ0quydGjJo7bOnp6aNWyir00bOVjkxctW7JolW6q6zBxEyWVuzbLr5KN2ayyzlVdyx LtFnCrdu4LNWbpddVwzRowatjVVZi5bLNXC5ddmzasmTNc1dNFWT+KP7WO++qdvFjCmGm6m9bJyi 7sUYqO1GVPM6yllLaKNnjvVTV4zGbRios1ZGLVFh799lmbB2Wbt27VksuycFlzBy0M04NDRg7anj R1107iJPv69MmDksvKOHa7Z6ZjVy443axMlXTh20Z52cqF2KxwssyXUFHjl6ZM2SdNtuWp4ycN27 Rk+E6I/VE/mQ/BH9UYB6C6NZJ9on7pP8gxhd/Ik+RNQ/vR5E+yPzR9SfzDMjdF0fCPmHeCeJHhR0 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