# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080724055504-ril13j8f5vdkel89 # target_branch: ../../repo/trunk-clean # testament_sha1: 9035bd89bd8d56da840e7e32479d8d691fc3b4d3 # timestamp: 2008-07-23 22:57:45 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080722152415-23v9fixusetjqy23 # # 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-07-21 09:40:01 +0000 +++ ChangeLog 2008-07-24 05:54:33 +0000 @@ -1,3 +1,17 @@ +2008-07-22 Colin D Bennett + + * include/grub/file.h (grub_file_buffering_enabled): New global + variable. + + * kern/file.c (BUFFER_SIZE): New constant. + (grub_file_buffering_enabled): New global variable. + (grub_file_open): Allocate file readahead buffer. + (grub_file_close): Free file readahead buffer. + (grub_file_read): Use file readahead buffer. + + * kern/main.c (grub_main): Enable file readahead buffering when it is + safe to do so. + 2008-07-21 Bean * kern/i386/pc/startup.S (gate_a20_try_bios): Change test order for @@ -337,6 +351,73 @@ * disk/ata.c: Use named constants for status bits. +2008-07-04 Colin D Bennett + + High resolution timer support. Implemented for i386 CPU using TSC. + Extracted generic grub_millisleep() so it's linked in only as needed. + This requires a Pentium compatible CPU; currently the code does not + check for this (so it will fail on 386 and 486 machines). + + * conf/i386-efi.rmk: Added TSC high resolution time module, link with + generic grub_millisleep() function. + + * conf/i386-pc.rmk: Likewise. + + * conf/sparc64-ieee1275.rmk: Add kern/generic/millisleep.c and + kern/generic/get_time_ms.c to kernel, to use generic time functions. + + * conf/powerpc-ieee1275.rmk: Add kern/generic/millisleep.c to kernel, + to use generic grub_millisleep() function. + + * conf/i386-linuxbios.rmk: Added kern/generic/get_time_ms.c to the + kernel. + + * kern/generic/get_time_ms.c (grub_get_time_ms): New file. Platform + independent implementation of grub_get_time_ms() using the RTC that + can be linked into a platform's kernel when it does not implement its + own specialized grub_get_time_ms() function. + + * kern/generic/millisleep.c (grub_millisleep): New file. Extracted + from grub_millisleep_generic() in kern/misc.c and renamed. Changed it + to use grub_get_time_ms() instead of grub_get_rtc() for better + precision on when high resolution time is available. + + * kern/misc.c (grub_millisleep_generic): Deleted. Moved to + kern/generic/millisleep.c so that it is only included in the kernel + image when a platform does not define a specialized version. + + * commands/sleep.c (grub_interruptible_millisleep): Uses + grub_get_time_ms() instead of grub_get_rtc() to stay in sync with + grub_millisleep() from kern/generic/millisleep.c. + + * include/grub/i386/tsc.h (grub_get_tsc): New file. Inline function + grub_get_tsc() uses x86 RDTSC instruction (available on Pentium+ CPUs) + to read the counter value for the TSC. + (grub_tsc_calibrate): Declare this function for grub_machine_init(). + + * kern/i386/tsc.c (grub_get_time_ms): x86 TSC support providing a high + resolution clock. + (grub_tsc_calibrate): New function to calibrate the TSC using RTC. + + * include/grub/time.h (grub_get_time_ms): Added grub_get_time_ms() + function to return the current time in millseconds since the epoch. + This supports higher resolution time than grub_get_rtc() on some + platforms such as i386-pc, where the RTC has only about 1/18 s + precision but a higher precision timer such as the TSC is available. + + * kern/i386/efi/init.c (grub_millisleep): Deleted. Don't define + grub_millisleep() -- it just called grub_millisleep_generic() but now + it is linked to kern/generic/millisleep.c for the implementation. + + * kern/sparc64/ieee1275/init.c (grub_millisleep): Deleted. + + * kern/i386/pc/init.c (grub_machine_init): Call grub_tsc_calibrate(). + (grub_millisleep): Deleted. + + * kern/ieee1275/init.c (grub_millisleep): Deleted. + (grub_get_rtc): Now calls grub_get_time_ms(), which does the real + work. + 2008-07-04 Pavel Roskin * kern/i386/linuxbios/init.c (grub_machine_init): Cast addr to === modified file 'Makefile.in' --- Makefile.in 2008-07-17 14:05:12 +0000 +++ Makefile.in 2008-07-19 21:39:24 +0000 @@ -85,6 +85,39 @@ YACC = @YACC@ UNIFONT_HEX = @UNIFONT_HEX@ +### Pretty output control ### +# Set up compiler and linker commands that either is quiet (does not print +# the command line being executed) or verbose (print the command line). +_CC := $(CC) +_TARGET_CC := $(TARGET_CC) +_STRIP := $(STRIP) +_GENMODSRC := sh $(srcdir)/genmodsrc.sh +ifeq ($(V),1) + override V_PREFIX := + override CC = $(_CC) + override TARGET_CC = $(_CC) + override STRIP = $(_STRIP) + override GENMODSRC = $(_GENMODSRC) + override INFO_GENCMDLIST = + override INFO_GENFSLIST = + override INFO_GENPARTMAPLIST = + override INFO_GEN_FINAL_COMMAND_LIST = + override INFO_GEN_FINAL_FS_LIST = + override INFO_GEN_FINAL_PARTMAP_LIST = +else + override V_PREFIX := @ + override CC = @echo "COMPILE $<"; $(_CC) + override TARGET_CC = @echo "COMPILE(TARGET) $<"; $(_TARGET_CC) + override STRIP = @echo "STRIP $@"; $(_STRIP) + override GENMODSRC = @echo "GENMODSRC $@"; $(_GENMODSRC) + override INFO_GENCMDLIST = @echo "GENCMDLIST $@" + override INFO_GENFSLIST = @echo "GENFSLIST $@" + override INFO_GENPARTMAPLIST = @echo "GENPARTMAPLIST $@" + override INFO_GEN_FINAL_COMMAND_LIST = @echo "GENCMDLIST[final] $@" + override INFO_GEN_FINAL_FS_LIST = @echo "GENFSLIST[final] $@" + override INFO_GEN_FINAL_PARTMAP_LIST = @echo "GENPARTMAPLIST[final] $@" +endif + # Options. enable_grub_emu = @enable_grub_emu@ enable_grub_fstest = @enable_grub_fstest@ @@ -132,13 +165,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-07-11 19:09:14 +0000 +++ conf/common.rmk 2008-07-24 05:36:41 +0000 @@ -269,6 +269,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 # For hello.mod. @@ -276,6 +277,15 @@ 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_mod_CFLAGS = $(COMMON_CFLAGS) +gfxmenu_mod_LDFLAGS = $(COMMON_LDFLAGS) + # For boot.mod. boot_mod_SOURCES = commands/boot.c boot_mod_CFLAGS = $(COMMON_CFLAGS) @@ -312,7 +322,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-efi.rmk' --- conf/i386-efi.rmk 2008-07-17 08:50:26 +0000 +++ conf/i386-efi.rmk 2008-07-19 21:39:45 +0000 @@ -84,7 +84,9 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/efi/init.c kern/parser.c kern/partition.c \ kern/env.c symlist.c kern/efi/efi.c kern/efi/init.c kern/efi/mm.c \ - term/efi/console.c disk/efi/efidisk.c + term/efi/console.c disk/efi/efidisk.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c kernel_mod_HEADERS = arg.h boot.h cache.h device.h disk.h dl.h elf.h elfload.h \ env.h err.h file.h fs.h kernel.h loader.h misc.h mm.h net.h parser.h \ partition.h pc_partition.h rescue.h symbol.h term.h time.h types.h \ === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-07-13 00:55:15 +0000 +++ conf/i386-pc.rmk 2008-07-19 21:46:40 +0000 @@ -43,12 +43,16 @@ kern/disk.c kern/dl.c kern/file.c kern/fs.c kern/err.c \ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/pc/init.c kern/parser.c kern/partition.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c \ kern/env.c \ + 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 +250,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 +265,10 @@ videotest_mod_LDFLAGS = $(COMMON_LDFLAGS) # For bitmap.mod -bitmap_mod_SOURCES = video/bitmap.c +bitmap_mod_SOURCES = video/bitmap.c \ + video/bitmap_scale_nn.c \ + video/bitmap_scale_bilinear.c \ + video/bitmap_scale.c bitmap_mod_CFLAGS = $(COMMON_CFLAGS) bitmap_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'conf/powerpc-ieee1275.rmk' --- conf/powerpc-ieee1275.rmk 2008-06-19 04:14:16 +0000 +++ conf/powerpc-ieee1275.rmk 2008-07-03 04:19:16 +0000 @@ -85,6 +85,7 @@ kern/ieee1275/init.c term/ieee1275/ofconsole.c \ kern/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/parser.c kern/partition.c kern/env.c kern/powerpc/dl.c \ + kern/generic/millisleep.c \ symlist.c kern/powerpc/cache.S kernel_elf_HEADERS = grub/powerpc/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) === modified file 'conf/sparc64-ieee1275.rmk' --- conf/sparc64-ieee1275.rmk 2008-06-19 00:04:59 +0000 +++ conf/sparc64-ieee1275.rmk 2008-07-03 14:16:34 +0000 @@ -73,6 +73,7 @@ kern/rescue.c kern/term.c term/ieee1275/ofconsole.c \ kern/sparc64/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/partition.c kern/env.c kern/sparc64/dl.c symlist.c \ + kern/generic/millisleep.c kern/generic/get_time_ms.c \ kern/sparc64/cache.S kern/parser.c kernel_elf_HEADERS = grub/sparc64/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) @@ -195,7 +196,7 @@ cat_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) === 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-03 14:12:08 +0000 @@ -0,0 +1,696 @@ +/* loader.c - Functions to handle loading fonts and glyphs from files. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +//#include +//#include +#include +#include +#include + +/* Definition of font registry. */ +struct font_node *grub_font_list; + +static int +register_font (grub_font_t font); + +static void +free_font (grub_font_t font); + +static void +remove_font (grub_font_t font); + +struct font_file_section +{ + grub_file_t file; /* The file this section is in. */ + char name[4]; /* FOURCC name of the section. */ + grub_uint32_t length; /* Length of the section contents. */ + int eof; /* Set by open_section() on EOF. */ +}; + +/* Font file format constants. */ +static const char pff2_magic[4] = { 'P', 'F', 'F', '2' }; +static const char section_names_file[4] = { 'F', 'I', 'L', 'E' }; +static const char section_names_font_name[4] = { 'N', 'A', 'M', 'E' }; +static const char section_names_max_char_width[4] = { 'M', 'A', 'X', 'W' }; +static const char section_names_max_char_height[4] = { 'M', 'A', 'X', 'H' }; +static const char section_names_ascent[4] = { 'A', 'S', 'C', 'E' }; +static const char section_names_descent[4] = { 'D', 'E', 'S', 'C' }; +static const char section_names_char_index[4] = { 'C', 'H', 'I', 'X' }; +static const char section_names_data[4] = { 'D', 'A', 'T', 'A' }; + +/* Replace unknown glyphs with a rounded question mark. */ +static grub_uint8_t unknown_glyph_bitmap[] = +{ + /* 76543210 */ + 0x7C, /* ooooo */ + 0x82, /* o o */ + 0xBA, /* o ooo o */ + 0xAA, /* o o o o */ + 0xAA, /* o o o o */ + 0x8A, /* o o o */ + 0x9A, /* o oo o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x7C, /* ooooo */ + 0x00 /* */ +}; + +static struct grub_font_glyph *unknown_glyph; + +void +grub_font_loader_init (void) +{ + unknown_glyph = grub_malloc(sizeof(struct grub_font_glyph) + + sizeof(unknown_glyph_bitmap)); + if (! unknown_glyph) + return; + + unknown_glyph->width = 8; + unknown_glyph->height = 16; + unknown_glyph->offset_x = 0; + unknown_glyph->offset_y = 0; + unknown_glyph->device_width = 8; + grub_memcpy(unknown_glyph->bitmap, + unknown_glyph_bitmap, sizeof(unknown_glyph_bitmap)); +} + +/* + * Open the next section in the file. + * + * On success, the section name is stored in section->name and the length in + * section->length, and 0 is returned. On failure, 1 is returned and + * grub_errno is set approriately with an error message. + * + * If 1 is returned due to being at the end of the file, then section->eof is + * set to 1; otherwise, section->eof is set to 0. + */ +static int +open_section (grub_file_t file, struct font_file_section *section) +{ + grub_ssize_t retval; + grub_uint32_t raw_length; + + section->file = file; + section->eof = 0; + + /* Read the FOURCC section name. */ + retval = grub_file_read (file, section->name, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section name"); + return 1; /* Read error. */ + } + + /* Read the big-endian 32-bit section length. */ + retval = grub_file_read (file, (char *) &raw_length, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section length"); + return 1; /* Read error. */ + } + + /* Convert byte-order and store in *length. */ + section->length = grub_be_to_cpu32 (raw_length); + + return 0; +} + +/* Size in bytes of each character index (CHIX section) + * entry in the font file. */ +#define FONT_CHAR_INDEX_ENTRY_SIZE (4 + 1 + 4) + +/* + * Load the character index (CHIX) section contents from the font file. This + * presumes that the position of FILE is positioned immediately after the + * section length for the CHIX section (i.e., at the start of the section + * contents). Returns 0 upon success, nonzero for failure (in which case + * grub_errno is set appropriately). + */ +static int +load_font_index (grub_file_t file, grub_uint32_t sect_length, struct + grub_font *font) +{ + unsigned i; + +#if FONT_DEBUG >= 2 + grub_printf("load_font_index(sect_length=%d)\n", sect_length); +#endif + + /* Sanity check: ensure section length is divisible by the entry size. */ + if (sect_length % FONT_CHAR_INDEX_ENTRY_SIZE != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: character index length %d " + "is not a multiple of the entry size %d", + sect_length, FONT_CHAR_INDEX_ENTRY_SIZE); + return 1; /* Invalid index section length. */ + } + + /* Calculate the number of characters. */ + font->num_chars = sect_length / FONT_CHAR_INDEX_ENTRY_SIZE; + + /* Allocate the character index array. */ + font->char_index = grub_malloc (font->num_chars + * sizeof (struct char_index_entry)); + if (!font->char_index) + return 1; /* Error allocating memory. */ + +#if FONT_DEBUG >= 2 + grub_printf("num_chars=%d)\n", font->num_chars); +#endif + + /* Load the character index data from the file. */ + for (i = 0; i < font->num_chars; i++) + { + struct char_index_entry *entry = &font->char_index[i]; + + /* Read code point value; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->code, 4) != 4) + return 1; + entry->code = grub_be_to_cpu32 (entry->code); + + /* Read storage flags byte. */ + if (grub_file_read (file, (char *) &entry->storage_flags, 1) != 1) + return 1; + + /* Read glyph data offset; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->offset, 4) != 4) + return 1; + entry->offset = grub_be_to_cpu32 (entry->offset); + + /* No glyph loaded. Will be loaded on demand and cached thereafter. */ + entry->glyph = 0; + +#if FONT_DEBUG >= 5 + if (i < 10) /* Print the 1st 10 characters. */ + grub_printf("c=%d o=%d\n", entry->code, entry->offset); +#endif + } + + return 0; /* Index loaded OK. */ +} + +/* + * Read the contents of the specified section as a string, which is + * allocated on the heap. Returns 0 if there is an error. + */ +static char * +read_section_as_string (struct font_file_section *section) +{ + char *str; + grub_ssize_t ret; + + str = grub_malloc (section->length + 1); + if (!str) + return 0; + + ret = grub_file_read (section->file, str, section->length); + if (ret < 0 || ret != (grub_ssize_t) section->length) + { + grub_free (str); + return 0; + } + + str[section->length] = '\0'; + return str; +} + +/* + * Read the contents of the current section as a 16-bit integer value, + * which is stored into *VALUE. Returns 0 upon success, nonzero upon failure. + */ +static int +read_section_as_short (struct font_file_section *section, grub_int16_t *value) +{ + grub_uint16_t raw_value; + + if (section->length != 2) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: section %c%c%c%c length " + "is %d but should be 2", + section->name[0], section->name[1], + section->name[2], section->name[3], + section->length); + return 1; /* An error occurred. */ + } + if (grub_file_read (section->file, (char *) &raw_value, 2) != 2) + return 1; /* An error occurred. */ + + *value = grub_be_to_cpu16 (raw_value); + return 0; /* Successfully read the value. */ +} + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int +grub_font_load (const char *filename) +{ + grub_file_t file = 0; + struct font_file_section section; + char magic[4]; + grub_font_t font = 0; + +#if FONT_DEBUG >= 1 + grub_printf("add_font(%s)\n", filename); +#endif + + file = grub_file_open (filename); + if (!file) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("file opened\n"); +#endif + + /* Read the FILE section. It indicates the file format. */ + if (open_section (file, §ion) != 0) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("opened FILE section\n"); +#endif + if (grub_memcmp (section.name, section_names_file, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: 1st section must be FILE"); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section name ok\n"); +#endif + if (section.length != 4) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error (file type ID length is %d " + "but should be 4)", section.length); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section length ok\n"); +#endif + /* Check the file format type code. */ + if (grub_file_read (file, magic, 4) != 4) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("read magic ok\n"); +#endif + + if (grub_memcmp (magic, pff2_magic, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, "Invalid font magic %x %x %x %x", + magic[0], magic[1], magic[2], magic[3]); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("compare magic ok\n"); +#endif + + /* Allocate the font object. */ + font = (grub_font_t) grub_malloc (sizeof (struct grub_font)); + if (!font) + goto fail; + + font->file = file; + font->name = 0; + font->ascent = 0; + font->descent = 0; + font->max_char_width = 0; + font->max_char_height = 0; + font->num_chars = 0; + font->char_index = 0; + +#if FONT_DEBUG >= 3 + grub_printf("allocate font ok; loading font info\n"); +#endif + + /* Load the font information. */ + while (1) + { + if (open_section (file, §ion) != 0) + { + if (section.eof) + break; /* Done reading the font file. */ + else + goto fail; + } + +#if FONT_DEBUG >= 2 + grub_printf("opened section %c%c%c%c ok\n", + section.name[0], section.name[1], + section.name[2], section.name[3]); +#endif + + if (grub_memcmp (section.name, section_names_font_name, 4) == 0) + { + font->name = read_section_as_string (§ion); + if (!font->name) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_width, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_width) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_height, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_height) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_ascent, 4) == 0) + { + if (read_section_as_short (§ion, &font->ascent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_descent, 4) == 0) + { + if (read_section_as_short (§ion, &font->descent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_char_index, 4) == 0) + { + /* Load the font index. */ + if (load_font_index (file, section.length, font) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_data, 4) == 0) + { + /* When the DATA section marker is reached, we stop reading. */ + break; + } + else + { + /* Unhandled section type, simply skip past it. */ +#if FONT_DEBUG >= 3 + grub_printf("Unhandled section type, skipping.\n"); +#endif + if ((int) grub_file_seek (file, + grub_file_tell (file) + section.length) + == -1) + goto fail; + } + } + + if (!font->name) + { + grub_printf ("Note: Font has no name.\n"); + font->name = grub_strdup ("Unknown"); + } + +#if FONT_DEBUG >= 1 + grub_printf ("Loaded font `%s'.\n" + "Ascent=%d Descent=%d MaxW=%d MaxH=%d Number of characters=%d.\n", + font->name, + font->ascent, font->descent, + font->max_char_width, font->max_char_height, + font->num_chars); +#endif + + if (font->max_char_width == 0 + || font->max_char_height == 0 + || font->num_chars == 0 + || font->char_index == 0 + || font->ascent == 0 + || font->descent == 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Invalid font file: missing some required data."); + goto fail; + } + + /* Add the font to the global font registry. */ + if (register_font (font) != 0) + goto fail; + + return 0; /* Font loaded ok. */ + +fail: + free_font (font); + return 1; /* Failed to load font. */ +} + +/* + * Read a 16-bit big-endian integer from FILE, convert it to native byte + * order, and store it in *VALUE. + * Returns 0 on success, 1 on failure. + */ +static int +read_be_uint16 (grub_file_t file, grub_uint16_t * value) +{ + if (grub_file_read (file, (char *) value, 2) != 2) + return 1; + *value = grub_be_to_cpu16 (*value); + return 0; +} + +static int +read_be_int16 (grub_file_t file, grub_int16_t * value) +{ + /* For the signed integer version, use the same code as for unsigned. */ + return read_be_uint16 (file, (grub_uint16_t *) value); +} + +/* + * Return a pointer to the character index entry for the glyph corresponding to + * the codepoint CODE in the font FONT. If not found, return zero. + */ +static struct char_index_entry * +find_glyph (const grub_font_t font, grub_uint32_t code) +{ + grub_uint32_t i; + grub_uint32_t len = font->num_chars; + struct char_index_entry *table = font->char_index; + + /* Do a linear search. */ + for (i = 0; i < len; i++) + { + if (table[i].code == code) + return &table[i]; + } + + return 0; /* No entry found for code point CODE. */ +} + +static struct grub_font_glyph * +grub_font_get_glyph_internal (grub_font_t font, grub_uint32_t code) +{ + struct char_index_entry *index_entry; + + index_entry = find_glyph (font, code); + if (index_entry) + { + struct grub_font_glyph *glyph = 0; + grub_uint16_t width; + grub_uint16_t height; + grub_int16_t xoff; + grub_int16_t yoff; + grub_int16_t dwidth; + int len; + + if (index_entry->glyph) + return index_entry->glyph; /* Return cached glyph. */ + + /* Make sure we can find glyphs for error messages. Push active + error message to error stack and reset error message. */ + grub_error_push (); + + grub_file_seek (font->file, index_entry->offset); + + /* Read the glyph width, height, and baseline. */ + if (read_be_uint16(font->file, &width) != 0 + || read_be_uint16(font->file, &height) != 0 + || read_be_int16(font->file, &xoff) != 0 + || read_be_int16(font->file, &yoff) != 0 + || read_be_int16(font->file, &dwidth) != 0) + { + //remove_font (font); + return 0; + } + + len = (width * height + 7) / 8; + glyph = grub_malloc (sizeof (struct grub_font_glyph) + len); + if (! glyph) + { + //remove_font (font); + return 0; + } + + glyph->font = font; + glyph->width = width; + glyph->height = height; + glyph->offset_x = xoff; + glyph->offset_y = yoff; + glyph->device_width = dwidth; + + /* Don't try to read empty bitmaps (e.g., space characters). */ + if (len != 0) + { + if (grub_file_read (font->file, (char *) glyph->bitmap, len) != len) + { + //remove_font (font); + return 0; + } + } + + /* Restore old error message. */ + grub_error_pop (); + + /* Cache the glyph. */ + index_entry->glyph = glyph; + + return glyph; /* Glyph loaded ok. */ + } + + return 0; +} + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph * +grub_font_get_glyph (grub_font_t font, grub_uint32_t code) +{ + struct grub_font_glyph *glyph; + glyph = grub_font_get_glyph_internal (font, code); + if (glyph == 0) + glyph = unknown_glyph; + return glyph; +} + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph * +grub_font_get_glyph_any (grub_uint32_t code) +{ + struct font_node *node; + /* Keep track of next node, in case there's an I/O error in + * grub_font_get_glyph() and the font is removed from the list. */ + struct font_node *next; + + for (node = grub_font_list; node; node = next) + { + grub_font_t font; + struct grub_font_glyph *glyph; + + font = node->value; + next = node->next; + + glyph = grub_font_get_glyph_internal (font, code); + if (glyph) + return glyph; + } + + /* Uggh... Glyph was not found in any font. */ + return unknown_glyph; /* Failed to load glyph. */ +} + +/* + * Free the memory used by a font. + * This should not be called if the font has been made available to + * users (once it is added to the global font list), since there would + * be the possibility of a dangling pointer. + */ +static void +free_font (grub_font_t font) +{ + if (font) + { + if (font->file) + grub_file_close (font->file); + if (font->name) + grub_free (font->name); + if (font->char_index) + grub_free (font->char_index); + grub_free (font); + } +} + +/* + * Add FONT to the global font registry. + * Returns 0 upon success, nonzero on failure (the font was not registered). + */ +static int +register_font (grub_font_t font) +{ + struct font_node *node = 0; + + node = grub_malloc (sizeof (struct font_node)); + if (!node) + return 1; /* Error. */ + + node->value = font; + node->next = grub_font_list; + grub_font_list = node; + + return 0; /* Success. */ +} + +/* + * Remove the font from the global font list. We don't actually free the + * font's memory since users could be holding references to the font. + */ +static void +remove_font (grub_font_t font) +{ + struct font_node **nextp, *cur; + + for (nextp = &grub_font_list, cur = *nextp; + cur; + nextp = &cur->next, cur = cur->next) + { + if (cur->value == font) + { + *nextp = cur->next; + + /* Free the node, but not the font itself. */ + grub_free (cur); + + return; + } + } +} + === removed file 'font/manager.c' --- font/manager.c 2008-02-10 11:04:38 +0000 +++ font/manager.c 1970-01-01 00:00:00 +0000 @@ -1,282 +0,0 @@ -/* - * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2005,2006,2007 Free Software Foundation, Inc. - * - * GRUB is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * GRUB is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with GRUB. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -struct entry -{ - grub_uint32_t code; - grub_uint32_t offset; -}; - -struct font -{ - struct font *next; - grub_file_t file; - grub_uint32_t num; - struct entry table[0]; -}; - -static struct font *font_list; - -/* Fill unknown glyph's with rounded question mark. */ -static grub_uint8_t unknown_glyph[16] = -{ /* 76543210 */ - 0x7C, /* ooooo */ - 0x82, /* o o */ - 0xBA, /* o ooo o */ - 0xAA, /* o o o o */ - 0xAA, /* o o o o */ - 0x8A, /* o o o */ - 0x9A, /* o oo o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x7C, /* ooooo */ - 0x00 /* */ -}; - -static int -add_font (const char *filename) -{ - grub_file_t file = 0; - char magic[4]; - grub_uint32_t num, i; - struct font *font = 0; - - file = grub_file_open (filename); - if (! file) - goto fail; - - if (grub_file_read (file, magic, 4) != 4) - goto fail; - - if (grub_memcmp (magic, GRUB_FONT_MAGIC, 4) != 0) - { - grub_error (GRUB_ERR_BAD_FONT, "invalid font magic"); - goto fail; - } - - if (grub_file_read (file, (char *) &num, 4) != 4) - goto fail; - - num = grub_le_to_cpu32 (num); - font = (struct font *) grub_malloc (sizeof (struct font) - + sizeof (struct entry) * num); - if (! font) - goto fail; - - font->file = file; - font->num = num; - - for (i = 0; i < num; i++) - { - grub_uint32_t code, offset; - - if (grub_file_read (file, (char *) &code, 4) != 4) - goto fail; - - if (grub_file_read (file, (char *) &offset, 4) != 4) - goto fail; - - font->table[i].code = grub_le_to_cpu32 (code); - font->table[i].offset = grub_le_to_cpu32 (offset); - } - - font->next = font_list; - font_list = font; - - return 1; - - fail: - if (font) - grub_free (font); - - if (file) - grub_file_close (file); - - return 0; -} - -static void -remove_font (struct font *font) -{ - struct font **p, *q; - - for (p = &font_list, q = *p; q; p = &(q->next), q = q->next) - if (q == font) - { - *p = q->next; - - grub_file_close (font->file); - grub_free (font); - - break; - } -} - -/* Return the offset of the glyph corresponding to the codepoint CODE - in the font FONT. If no found, return zero. */ -static grub_uint32_t -find_glyph (const struct font *font, grub_uint32_t code) -{ - grub_uint32_t start = 0; - grub_uint32_t end = font->num - 1; - const struct entry *table = font->table; - - /* This shouldn't happen. */ - if (font->num == 0) - return 0; - - /* Do a binary search. */ - while (start <= end) - { - grub_uint32_t i = (start + end) / 2; - - if (table[i].code < code) - start = i + 1; - else if (table[i].code > code) - end = i - 1; - else - return table[i].offset; - } - - return 0; -} - -/* Set the glyph to something stupid. */ -static void -fill_with_default_glyph (grub_font_glyph_t glyph) -{ - unsigned i; - - /* Use pre-defined pattern to fill unknown glyphs. */ - for (i = 0; i < 16; i++) - glyph->bitmap[i] = unknown_glyph[i]; - - glyph->char_width = 1; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; -} - -/* Get a glyph corresponding to the codepoint CODE. Always fill glyph - information with something, even if no glyph is found. */ -int -grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph) -{ - struct font *font; - grub_uint8_t bitmap[32]; - - /* FIXME: It is necessary to cache glyphs! */ - - restart: - for (font = font_list; font; font = font->next) - { - grub_uint32_t offset; - - offset = find_glyph (font, code); - if (offset) - { - grub_uint32_t w; - int len; - - /* Make sure we can find glyphs for error messages. Push active - error message to error stack and reset error message. */ - grub_error_push (); - - grub_file_seek (font->file, offset); - if ((len = grub_file_read (font->file, (char *) &w, sizeof (w))) - != sizeof (w)) - { - remove_font (font); - goto restart; - } - - w = grub_le_to_cpu32 (w); - if (w != 1 && w != 2) - { - /* grub_error (GRUB_ERR_BAD_FONT, "invalid width"); */ - remove_font (font); - goto restart; - } - - if (grub_file_read (font->file, (char *) bitmap, w * 16) - != (grub_ssize_t) w * 16) - { - remove_font (font); - goto restart; - } - - /* Fill glyph with information. */ - grub_memcpy (glyph->bitmap, bitmap, w * 16); - - glyph->char_width = w; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; - - /* Restore old error message. */ - grub_error_pop (); - - return 1; - } - } - - /* Uggh... No font was found. */ - fill_with_default_glyph (glyph); - return 0; -} - -static grub_err_t -font_command (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ - if (argc == 0) - return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); - - while (argc--) - if (! add_font (*args++)) - return 1; - - return 0; -} - -GRUB_MOD_INIT(font_manager) -{ - grub_register_command ("font", font_command, GRUB_COMMAND_FLAG_BOTH, - "font FILE...", - "Specify one or more font files to display.", 0); -} - -GRUB_MOD_FINI(font_manager) -{ - grub_unregister_command ("font"); -} === modified file 'genmk.rb' --- genmk.rb 2008-07-02 18:03:23 +0000 +++ genmk.rb 2008-07-04 21:40:36 +0000 @@ -125,7 +125,7 @@ $(TARGET_CC) $(TARGET_CPPFLAGS) $(TARGET_CFLAGS) $(#{prefix}_CFLAGS) -c -o $@ $< #{mod_src}: moddep.lst genmodsrc.sh - sh $(srcdir)/genmodsrc.sh '#{mod_name}' $< > $@ || (rm -f $@; exit 1) + $(GENMODSRC) '#{mod_name}' $< > $@ || (rm -f $@; exit 1) ifneq ($(#{prefix}_EXPORTS),no) #{defsym}: #{pre_obj} @@ -157,18 +157,21 @@ PARTMAPFILES += #{partmap} #{command}: #{src} $(#{src}_DEPENDENCIES) gencmdlist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENCMDLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/gencmdlist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{fs}: #{src} $(#{src}_DEPENDENCIES) genfslist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENFSLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genfslist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{partmap}: #{src} $(#{src}_DEPENDENCIES) genpartmaplist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENPARTMAPLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genpartmaplist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) === added directory 'gfxmenu' === added file 'gfxmenu/gfxmenu.c' --- gfxmenu/gfxmenu.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gfxmenu.c 2008-07-24 05:36:41 +0000 @@ -0,0 +1,155 @@ +/* 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 grub_err_t +show_menu (grub_menu_t menu, int nested __attribute__ ((unused))) +{ + grub_gfxmenu_model_t model; + + model = grub_gfxmenu_model_new (menu); + if (! model) + { + grub_print_error (); + grub_wait_after_message (); + return grub_errno; + } + + grub_gfxmenu_view_t view; + + view = grub_gfxmenu_view_new ("/boot/theme.txt", model); + if (! view) + { + grub_print_error (); + grub_wait_after_message (); + grub_gfxmenu_model_destroy (model); + return grub_errno; + } + + grub_gfxmenu_model_set_selected_index (model, 0); + + int done = 0; + while (!done && !grub_menu_viewer_should_return ()) + { + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + + int c = GRUB_TERM_ASCII_CHAR (grub_getkey ()); + 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) + goto fail; + } + else if (c == 't') + { + /* The write hook for 'menuviewer' will cause + * grub_menu_viewer_should_return to return nonzero. */ + grub_env_set ("menuviewer", "terminal"); + } + else if (nested && c == GRUB_TERM_ESC) + { + done = 1; + } + } + +fail: + 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-24 05:36:41 +0000 @@ -0,0 +1,135 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include + +/* Model type definition. */ +struct grub_gfxmenu_model +{ + int num_entries; + grub_menu_entry_t *entries; + int selected_entry_index; +}; + + +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->entries = 0; + model->selected_entry_index = 0; + model->num_entries = menu->size; + 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); +} + +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/view.c' --- gfxmenu/view.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/view.c 2008-07-24 05:36:41 +0000 @@ -0,0 +1,694 @@ +/* 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 + +struct grub_gfxmenu_view +{ + grub_video_rect_t screen; + + int icon_width; + int icon_height; + int item_height; + grub_font_t title_font; + grub_font_t item_font; + grub_font_t status_font; + grub_video_color_t title_color; + grub_video_color_t item_color; + grub_video_color_t status_color; + grub_video_color_t status_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; + char *screen_title; + + 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); + +/* TODO Make image files referenced by a theme relative to theme path. */ +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 (100, 100, 100), + view->screen.x, view->screen.y, + view->screen.width, view->screen.height); + grub_video_swap_buffers (); + + init_terminal (view); + + 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->title_font = default_font; + view->item_font = default_font; + view->status_font = default_font; + view->title_color = default_fg_color; + view->item_color = default_fg_color; + view->status_color = default_bg_color; + view->status_bg_color = default_fg_color; + view->desktop_image = 0; + view->desktop_color = default_bg_color; + view->menu_box = 0; + view->selected_item_box = 0; + view->screen_title = grub_strdup ("GRUB Boot Menu"); + + view->model = model; + + grub_gfxmenu_view_load_theme (view, theme_path); + + return view; +} + +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); + grub_free (view->screen_title); + grub_free (view); + + set_text_mode (); + destroy_terminal (); +} + +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; +} + +/* Return zero on failure, nonzero on success. */ +static int +theme_set_box (grub_gfxmenu_box_t *boxptr, char *pattern) +{ + char *prefix; + char *suffix; + char *star; + grub_gfxmenu_box_t box; + + star = grub_strchr (pattern, '*'); + if (! star) + return 0; + + prefix = grub_malloc (star - pattern + 1); + if (! prefix) + return 0; + grub_memcpy (prefix, pattern, star - pattern); + prefix[star - pattern] = '\0'; + suffix = star + 1; + box = grub_gfxmenu_create_box (prefix, suffix); + 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) +{ + 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 ("status-font", name)) + view->status_font = grub_font_get (value); + 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 ("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 ("desktop-image", name)) + { + struct grub_video_bitmap *raw_bitmap; + struct grub_video_bitmap *scaled_bitmap; + if (grub_video_bitmap_load (&raw_bitmap, value) != GRUB_ERR_NONE) + goto fail; + 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); + else if (! grub_strcmp ("selected-item-box", name)) + theme_set_box (&view->selected_item_box, value); + +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; + + grub_free (name); +} + +static char *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; +} + +/* 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) +{ +#if 1 + grub_file_t file; + char *buf; + int pos; + int len; + + file = grub_file_open (theme_path); + if (!file) + return 0; + + len = grub_file_size (file); + buf = grub_malloc (len); + if (! buf) + { + grub_file_close (file); + return 0; + } + if (grub_file_read (file, buf, len) != len) + { + grub_free (buf); + grub_file_close (file); + 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, + new_substring (buf, name_start, name_end), + new_substring (buf, value_start, value_end)); + } + 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 = 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, + 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 (buf); + grub_file_close (file); + return 1; +#else + theme_set_string (view, + grub_strdup ("title-font"), + grub_strdup ("Helvetica Bold 24")); + theme_set_string (view, + grub_strdup ("item-font"), + grub_strdup ("Helvetica Bold 12")); + theme_set_string (view, + grub_strdup ("status-font"), + grub_strdup ("Helvetica 12")); + theme_set_string (view, + grub_strdup ("title-color"), + grub_strdup ("255,255,0")); + theme_set_string (view, + grub_strdup ("item-color"), + grub_strdup ("0,0,255")); + theme_set_string (view, + grub_strdup ("status-color"), + grub_strdup ("255,255,0")); + theme_set_string (view, + grub_strdup ("status-bg-color"), + grub_strdup ("64,64,64")); + theme_set_string (view, + grub_strdup ("menu-box"), + grub_strdup ("/boot/images/menubox_*.tga")); + theme_set_string (view, + grub_strdup ("selected-item-box"), + grub_strdup ("/boot/images/select_blue_*.tga")); + return 1; +#endif +} + +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 struct grub_video_bitmap * +get_item_icon (grub_gfxmenu_view_t view __attribute__((unused)), + int item_index __attribute__((unused))) +{ + /* TODO Implement icons. */ + return 0; +} + +static void +draw_menu (grub_gfxmenu_view_t view) +{ + int boxpad = 14; + int icon_text_space = 4; + int item_vspace = 16; + + 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++) + { + if (i == grub_gfxmenu_model_get_selected_index (view->model)) + { + 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); + + grub_video_draw_string (grub_gfxmenu_model_get_entry_title (view->model, i), + view->item_font, 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->screen_title) + return; + + /* Center the title. */ + int title_width = grub_font_get_string_width (view->title_font, + view->screen_title); + int x = (view->screen.width - title_width) / 2; + int y = 40 + grub_font_get_ascent (view->title_font); + grub_video_draw_string (view->screen_title, + 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); +} + +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); +} + +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 grub_gfxmenu_box_t term_box; +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))) +{ + 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; + int termy = term_view->screen.y + term_view->screen.height / 10; + + if (term_box) + 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 * 8 / 10; + term_target_height = view->screen.height * 8 / 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; + + grub_gfxterm_init_window (term_target, 0, 0, + term_target_width, term_target_height, + "Fixed 10", 16); + if (grub_errno != GRUB_ERR_NONE) + return; + + term_box = grub_gfxmenu_create_box ("/boot/images/select_blue_", ".tga"); + term_box->set_content_size (term_box, term_target_width, term_target_height); + + /* 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_box->destroy (term_box); + term_box = 0; + grub_gfxterm_destroy_window (); + grub_gfxterm_set_repaint_callback (0); + grub_video_delete_render_target (term_target); + grub_term_set_current (term_original); +} + +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/file.h' --- include/grub/file.h 2007-08-02 17:40:37 +0000 +++ include/grub/file.h 2008-07-22 15:23:31 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2002,2007 Free Software Foundation, Inc. + * Copyright (C) 2002,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 @@ -39,6 +39,19 @@ /* The file size. */ grub_off_t size; + /* The file offset when `buffer' starts, or -1 to indicate that + buffer has no data. */ + grub_off_t buffer_offset; + + /* The number of bytes in `buffer'. */ + grub_size_t buffer_length; + + /* The number of bytes allocated for `buffer'. */ + grub_size_t buffer_capacity; + + /* Readahead buffer. */ + char *buffer; + /* Filesystem-specific data. */ void *data; @@ -48,6 +61,11 @@ }; typedef struct grub_file *grub_file_t; +/* Enable file readahead buffering if nonzero. Buffering is disabled by + default, since it seems to cause problems during the early GRUB boot + process. The grub_main function enables it. */ +extern int grub_file_buffering_enabled; + /* Get a device name from NAME. */ char *EXPORT_FUNC(grub_file_get_device_name) (const char *name); === 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-24 05:36:41 +0000 @@ -0,0 +1,45 @@ +/* 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 + +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); + +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-24 05:36:41 +0000 @@ -0,0 +1,50 @@ +/* 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_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-04 17:55:21 +0000 @@ -0,0 +1,43 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef KERNEL_CPU_TSC_HEADER +#define KERNEL_CPU_TSC_HEADER 1 + +#include + +/* Read the TSC value, which increments with each CPU clock cycle. */ +static __inline grub_uint64_t +grub_get_tsc (void) +{ + grub_uint32_t lo, hi; + + /* The CPUID instruction is a 'serializing' instruction, and + avoids out-of-order execution of the RDTSC instruction. */ + __asm__ __volatile__ ("xorl %%eax, %%eax\n\t" + "cpuid":::"%rax", "%rbx", "%rcx", "%rdx"); + /* Read TSC value. We cannot use "=A", since this would use + %rax on x86_64. */ + __asm__ __volatile__ ("rdtsc":"=a" (lo), "=d" (hi)); + + return (((grub_uint64_t) hi) << 32) | lo; +} + +void grub_tsc_calibrate (void); + +#endif /* ! KERNEL_CPU_TSC_HEADER */ === added file 'include/grub/menu.h' --- include/grub/menu.h 1970-01-01 00:00:00 +0000 +++ include/grub/menu.h 2008-07-19 18:40:28 +0000 @@ -0,0 +1,51 @@ +/* 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 + +/* 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; + +#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-07-19 19:24:29 +0000 @@ -25,6 +25,7 @@ #include #include #include +#include /* The maximum size of a command-line. */ #define GRUB_MAX_CMDLINE 1600 @@ -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,12 @@ /* To exit from the normal mode. */ extern grub_jmp_buf grub_exit_env; +extern struct grub_menu_viewer grub_normal_terminal_menu_viewer; + 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_entry_run (grub_menu_entry_t entry); +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); === modified file 'include/grub/time.h' --- include/grub/time.h 2007-10-22 19:02:16 +0000 +++ include/grub/time.h 2008-07-03 04:19:16 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2007 Free Software Foundation, Inc. + * Copyright (C) 2007, 2008 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,12 +19,13 @@ #ifndef KERNEL_TIME_HEADER #define KERNEL_TIME_HEADER 1 +#include #include #include #include void EXPORT_FUNC(grub_millisleep) (grub_uint32_t ms); -void EXPORT_FUNC(grub_millisleep_generic) (grub_uint32_t ms); +grub_uint64_t EXPORT_FUNC(grub_get_time_ms) (void); static __inline void grub_sleep (grub_uint32_t s) === modified file 'include/grub/video.h' --- include/grub/video.h 2008-01-01 12:02:07 +0000 +++ include/grub/video.h 2008-07-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 */ === modified file 'kern/file.c' --- kern/file.c 2008-01-25 20:57:40 +0000 +++ kern/file.c 2008-07-22 15:23:31 +0000 @@ -1,7 +1,7 @@ /* file.c - file I/O functions */ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2002,2006,2007 Free Software Foundation, Inc. + * Copyright (C) 2002,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 @@ -24,6 +24,12 @@ #include #include +/* File readahead buffer size in bytes. */ +#define BUFFER_SIZE 1024 + +/* Use readahead buffering for file reads? */ +int grub_file_buffering_enabled = 0; + /* Get the device part of the filename NAME. It is enclosed by parentheses. */ char * grub_file_get_device_name (const char *name) @@ -81,6 +87,10 @@ file->device = device; file->offset = 0; + file->buffer_offset = -1; + file->buffer_length = 0; + file->buffer_capacity = 0; + file->buffer = 0; file->data = 0; file->read_hook = 0; @@ -97,6 +107,13 @@ if ((file->fs->open) (file, file_name) != GRUB_ERR_NONE) goto fail; + if (grub_file_buffering_enabled) + { + file->buffer = grub_malloc (BUFFER_SIZE); + if (file->buffer) + file->buffer_capacity = BUFFER_SIZE; + } + return file; fail: @@ -110,6 +127,15 @@ return 0; } +static inline grub_size_t +grub_min (grub_size_t a, grub_size_t b) +{ + if (a < b) + return a; + else + return b; +} + grub_ssize_t grub_file_read (grub_file_t file, char *buf, grub_size_t len) { @@ -124,10 +150,52 @@ if (len == 0) return 0; - - res = (file->fs->read) (file, buf, len); - if (res > 0) - file->offset += res; + + if (grub_file_buffering_enabled && file->buffer) + { + res = 0; /* The while loop uses res to accumulate the # bytes read. */ + while (len) + { + /* If the start of the chunk requested is not in the buffer, + then fill the buffer starting at the requested offset. */ + if (file->buffer_offset == -1 + || file->offset < file->buffer_offset + || file->offset >= file->buffer_offset + file->buffer_length) + { + grub_ssize_t res2; + + /* Read into the buffer, and then use that data. */ + file->buffer_offset = -1; + file->buffer_length = grub_min (file->buffer_capacity, + file->size - file->offset); + res2 = (file->fs->read) (file, file->buffer, file->buffer_length); + if (res2 > 0) + { + file->buffer_length = res2; + file->buffer_offset = file->offset; + } + else + return res; + } + + /* Use as much buffered data as possible. */ + grub_size_t bufstart = file->offset - file->buffer_offset; + grub_size_t buflen = len; + if (bufstart + len > file->buffer_length) + buflen = file->buffer_length - bufstart; + grub_memcpy (buf, file->buffer + bufstart, buflen); + buf += buflen; + res += buflen; + len -= buflen; + file->offset += buflen; + } + } + else + { + res = (file->fs->read) (file, buf, len); + if (res > 0) + file->offset += res; + } return res; } @@ -135,6 +203,9 @@ grub_err_t grub_file_close (grub_file_t file) { + grub_free (file->buffer); + file->buffer = 0; + if (file->fs->close) (file->fs->close) (file); === added directory 'kern/generic' === added file 'kern/generic/get_time_ms.c' --- kern/generic/get_time_ms.c 1970-01-01 00:00:00 +0000 +++ kern/generic/get_time_ms.c 2008-07-04 16:55:48 +0000 @@ -0,0 +1,37 @@ +/* get_time_ms.c - generic time implementation -- using platform RTC. + * The generic implementation of these functions can be used for architectures + * or platforms that do not have a more specialized implementation. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include + +/* Calculate the time in milliseconds since the epoch based on the RTC. */ +grub_uint64_t +grub_get_time_ms (void) +{ + /* By dimensional analysis: + + 1000 ms N rtc ticks 1 s + ------- * ----------- * ----------- = 1000*N/T ms + 1 s 1 T rtc ticks + */ + grub_uint64_t ticks_ms_per_sec = ((grub_uint64_t) 1000) * grub_get_rtc (); + return grub_divmod64 (ticks_ms_per_sec, GRUB_TICKS_PER_SECOND, 0); +} === added file 'kern/generic/millisleep.c' --- kern/generic/millisleep.c 1970-01-01 00:00:00 +0000 +++ kern/generic/millisleep.c 2008-07-04 16:55:48 +0000 @@ -0,0 +1,39 @@ +/* millisleep.c - generic millisleep function. + * The generic implementation of these functions can be used for architectures + * or platforms that do not have a more specialized implementation. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 1999,2000,2001,2002,2003,2004,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include + +void +grub_millisleep (grub_uint32_t ms) +{ + grub_uint64_t start; + + start = grub_get_time_ms (); + + /* Instead of setting an end time and looping while the current time is + less than that, comparing the elapsed sleep time with the desired sleep + time handles the (unlikely!) case that the timer would wrap around + during the sleep. */ + + while (grub_get_time_ms () - start < ms) + grub_cpu_idle (); +} === modified file 'kern/i386/efi/init.c' --- kern/i386/efi/init.c 2007-10-22 18:59:33 +0000 +++ kern/i386/efi/init.c 2008-07-04 17:55:21 +0000 @@ -25,18 +25,13 @@ #include #include #include -#include - -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} +#include void grub_machine_init (void) { grub_efi_init (); + grub_tsc_calibrate (); } void === modified file 'kern/i386/pc/init.c' --- kern/i386/pc/init.c 2008-06-15 17:21:16 +0000 +++ kern/i386/pc/init.c 2008-07-04 18:03:26 +0000 @@ -30,6 +30,7 @@ #include #include #include +#include struct mem_region { @@ -46,12 +47,6 @@ grub_size_t grub_os_area_size; grub_size_t grub_lower_mem, grub_upper_mem; -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - void grub_arch_sync_caches (void *address __attribute__ ((unused)), grub_size_t len __attribute__ ((unused))) @@ -231,6 +226,8 @@ if (! grub_os_area_addr) grub_fatal ("no upper memory"); + + grub_tsc_calibrate (); } void === added file 'kern/i386/tsc.c' --- kern/i386/tsc.c 1970-01-01 00:00:00 +0000 +++ kern/i386/tsc.c 2008-07-04 17:55:21 +0000 @@ -0,0 +1,89 @@ +/* kern/i386/tsc.c - x86 TSC time source implementation + * Requires Pentium or better x86 CPU that supports the RDTSC instruction. + * This module uses the RTC (via grub_get_rtc()) to calibrate the TSC to + * real time. + * + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +/* Calibrated reference for TSC=0. This defines the time since the epoch in + milliseconds that TSC=0 refers to. */ +static grub_uint64_t tsc_boot_time; + +/* Calibrated TSC rate. (In TSC ticks per millisecond.) */ +static grub_uint64_t tsc_ticks_per_ms; + + +/* Declared in . */ +grub_uint64_t +grub_get_time_ms (void) +{ + return tsc_boot_time + grub_divmod64 (grub_get_tsc (), tsc_ticks_per_ms, 0); +} + + +/* How many RTC ticks to use for calibration loop. (>= 1) */ +#define CALIBRATION_TICKS 2 + +/* Calibrate the TSC based on the RTC. */ +void +grub_tsc_calibrate (void) +{ + /* First calbrate the TSC rate (relative, not absolute time). */ + grub_uint64_t start_tsc; + grub_uint64_t end_tsc; + grub_uint32_t initial_tick; + grub_uint32_t start_tick; + grub_uint32_t end_tick; + + /* Wait for the start of the next tick; + we'll base out timing off this edge. */ + initial_tick = grub_get_rtc (); + do + { + start_tick = grub_get_rtc (); + } + while (start_tick == initial_tick); + start_tsc = grub_get_tsc (); + + /* Wait for the start of the next tick. This will + be the end of the 1-tick period. */ + do + { + end_tick = grub_get_rtc (); + } + while (end_tick - start_tick < CALIBRATION_TICKS); + end_tsc = grub_get_tsc (); + + tsc_ticks_per_ms = + grub_divmod64 (grub_divmod64 + (end_tsc - start_tsc, end_tick - start_tick, 0) + * GRUB_TICKS_PER_SECOND, 1000, 0); + + /* Reference the TSC zero (boot time) to the epoch to + get an absolute real time reference. */ + grub_uint64_t ms_since_boot = grub_divmod64 (end_tsc, tsc_ticks_per_ms, 0); + grub_uint64_t mstime_now = grub_divmod64 ((grub_uint64_t) 1000 * end_tick, + GRUB_TICKS_PER_SECOND, + 0); + tsc_boot_time = mstime_now - ms_since_boot; +} === modified file 'kern/ieee1275/init.c' --- kern/ieee1275/init.c 2008-07-04 02:01:55 +0000 +++ kern/ieee1275/init.c 2008-07-04 19:43:25 +0000 @@ -47,12 +47,6 @@ extern char _end[]; void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - -void grub_exit (void) { grub_ieee1275_exit (); @@ -260,8 +254,8 @@ grub_console_fini (); } -grub_uint32_t -grub_get_rtc (void) +grub_uint64_t +grub_get_time_ms (void) { grub_uint32_t msecs = 0; @@ -270,6 +264,12 @@ return msecs; } +grub_uint32_t +grub_get_rtc (void) +{ + return grub_get_time_ms (); +} + grub_addr_t grub_arch_modules_addr (void) { === modified file 'kern/main.c' --- kern/main.c 2008-06-19 19:08:57 +0000 +++ kern/main.c 2008-07-24 05:54:33 +0000 @@ -128,6 +128,9 @@ grub_env_export ("prefix"); grub_set_root_dev (); + /* Enable file readahead buffering, now that it is safe to do so. */ + grub_file_buffering_enabled = 1; + /* Load the normal mode module. */ grub_load_normal_mode (); === 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-04 17:55:21 +0000 @@ -66,12 +66,6 @@ /* Never reached. */ } -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - int grub_ieee1275_test_flag (enum grub_ieee1275_flag flag) { === modified file 'normal/main.c' --- normal/main.c 2008-02-02 16:48:52 +0000 +++ normal/main.c 2008-07-19 19:07:47 +0000 @@ -28,6 +28,7 @@ #include #include #include +#include grub_jmp_buf grub_exit_env; @@ -476,7 +477,7 @@ if (menu && menu->size) { - grub_menu_run (menu, nested); + grub_menu_viewer_show_menu (menu, nested); if (nested) free_menu (menu); } @@ -519,6 +520,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 +538,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-19 19:24:29 +0000 @@ -24,6 +24,7 @@ #include #include #include +#include static grub_uint8_t grub_color_menu_normal; static grub_uint8_t grub_color_menu_highlight; @@ -364,7 +365,7 @@ if (timeout > 0) print_timeout (timeout, offset, 0); - while (1) + while (!grub_menu_viewer_should_return ()) { int c; timeout = get_timeout (); @@ -468,6 +469,10 @@ } goto refresh; + case 't': + grub_env_set ("menuviewer", "protomenu"); + goto refresh; + default: break; } @@ -476,13 +481,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,8 +497,8 @@ grub_command_execute ("boot", 0); } -void -grub_menu_run (grub_menu_t menu, int nested) +static grub_err_t +show_menu (grub_menu_t menu, int nested) { while (1) { @@ -513,7 +519,7 @@ grub_printf (" Booting \'%s\'\n\n", e->title); - run_menu_entry (e); + grub_menu_execute_entry (e); /* Deal with a fallback entry. */ /* FIXME: Multiple fallback entries like GRUB Legacy. */ @@ -526,7 +532,7 @@ 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); + grub_menu_execute_entry (e); } if (grub_errno != GRUB_ERR_NONE) @@ -537,4 +543,12 @@ grub_wait_after_message (); } } + + 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-07-03 14:27:43 +0000 @@ -0,0 +1,99 @@ +/* bitmap_scale.c - Bitmap scaling interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * This function creates a new scaled version of the bitmap SRC. The new + * bitmap has dimensions DST_WIDTH by DST_HEIGHT. The scaling algorithm + * is given by SCALE_METHOD. If an error is encountered, the return code is + * not equal to GRUB_ERR_NONE, and the bitmap DST is either not created, or + * it is destroyed before this function returns. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum grub_video_bitmap_scale_method + scale_method) +{ + + /* Verify the simplifying assumptions. */ + if (src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "null src bitmap in grub_video_bitmap_create_scaled"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "src format not supported for scale"); + if (src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + if (dst_width <= 0 || dst_height <= 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "requested to scale to a size w/ a zero dimension"); + if (src->mode_info.bytes_per_pixel * 8 != src->mode_info.bpp) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "bitmap to scale has inconsistent Bpp and bpp"); + + /* Create the new bitmap. */ + grub_err_t ret; + ret = grub_video_bitmap_create (dst, dst_width, dst_height, + src->mode_info.blit_format); + if (ret != GRUB_ERR_NONE) + return ret; /* Error. */ + + switch (scale_method) + { + case GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST: + ret = grub_video_bitmap_scale_nn (*dst, src); + break; + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR: + ret = grub_video_bitmap_scale_bilinear (*dst, src); + break; + default: + ret = grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid scale_method value"); + break; + } + + if (ret == GRUB_ERR_NONE) + { + /* Success: *dst is now a pointer to the scaled bitmap. */ + return GRUB_ERR_NONE; + } + else + { + /* Destroy the bitmap and return the error code. */ + grub_video_bitmap_destroy (*dst); + return ret; + } +} === added file 'video/bitmap_scale_bilinear.c' --- video/bitmap_scale_bilinear.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_bilinear.c 2008-07-03 14:28:15 +0000 @@ -0,0 +1,145 @@ +/* bitmap_scale_bilinear.c - Bilinear image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the bilinear interpolation algorithm + * to interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale func"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* If we have enough space to do so, use bilinear interpolation. + * Otherwise, fall back to nearest neighbor for this pixel. */ + if (sx < sw - 1 && sy < sh - 1) + { + /* Do bilinear interpolation. */ + + /* Fixed-point .8 numbers representing the fraction of the + * distance in the x (u) and y (v) direction within the + * box of 4 pixels in the source. */ + int u = (256 * sw * dx / dw) - (sx * 256); + int v = (256 * sh * dy / dh) - (sy * 256); + + for (comp = 0; comp < bytes_per_pixel; comp++) + { + /* Get the component's values for the + * 4 source corner pixels. */ + grub_uint8_t f00 = sptr[comp]; + grub_uint8_t f10 = sptr[comp + bytes_per_pixel]; + grub_uint8_t f01 = sptr[comp + sstride]; + grub_uint8_t f11 = sptr[comp + sstride + bytes_per_pixel]; + + /* Do linear interpolations along the top and bottom + * rows of the box. */ + grub_uint8_t f0y = (256 - v) * f00 / 256 + v * f01 / 256; + grub_uint8_t f1y = (256 - v) * f10 / 256 + v * f11 / 256; + + /* Interpolate vertically. */ + grub_uint8_t fxy = (256 - u) * f0y / 256 + u * f1y / 256; + + dptr[comp] = fxy; + } + } + else + { + /* Fall back to nearest neighbor interpolation. */ + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + } + return GRUB_ERR_NONE; +} === added file 'video/bitmap_scale_nn.c' --- video/bitmap_scale_nn.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_nn.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,109 @@ +/* bitmap_scale_nn.c - Nearest neighbor image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the nearest neighbor algorithm to + * interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale_nn"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + return GRUB_ERR_NONE; +} === modified file 'video/i386/pc/vbe.c' --- video/i386/pc/vbe.c 2008-01-21 15:48:27 +0000 +++ video/i386/pc/vbe.c 2008-07-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-01-22 13:50:38 +0000 +++ video/readers/jpeg.c 2008-07-03 13:49:18 +0000 @@ -542,7 +542,7 @@ if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8)) + GRUB_VIDEO_BLIT_FORMAT_RGB_888)) return grub_errno; data->bit_mask = 0x0; === modified file 'video/readers/png.c' --- video/readers/png.c 2008-03-31 11:00:48 +0000 +++ video/readers/png.c 2008-07-03 13:49:18 +0000 @@ -231,7 +231,7 @@ { if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8)) + GRUB_VIDEO_BLIT_FORMAT_RGB_888)) return grub_errno; data->bpp = 3; } @@ -239,7 +239,7 @@ { if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8)) + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888)) return grub_errno; data->bpp = 4; } === modified file 'video/readers/tga.c' --- video/readers/tga.c 2007-12-30 08:52:06 +0000 +++ video/readers/tga.c 2008-07-03 13:49:18 +0000 @@ -397,7 +397,7 @@ { grub_video_bitmap_create (bitmap, header.image_width, header.image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8); + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888); if (grub_errno != GRUB_ERR_NONE) { grub_file_close (file); @@ -420,7 +420,7 @@ { grub_video_bitmap_create (bitmap, header.image_width, header.image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8); + GRUB_VIDEO_BLIT_FORMAT_RGB_888); if (grub_errno != GRUB_ERR_NONE) { grub_file_close (file); === 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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 Rl+dETS7viG2QhuYWhYYm/y9Bt5Ne7rlrY4UCp73s4VTxQQQ4naGnLQlodgB2py9Cr+pyNw7zo6N HDntw2I438huqP3lIK7Ix0n/maR0DTNgS09eXjbcGawY4Nt2YuyAgeTvucp3PO62WDo2ULBJEIMF q3A+0jcXhw3clhtaSvFKheqTaqaxEyBerBpADnt0TqEbqFhxYqW16jkQSzve389PhaHP15jOjRqU JzFWK+9wyso53j40r4eGE48PFEqtlg3wxo/uiHNXQYcrKiSg7ZRBqIFG/mUpVzLumeLid17QK569 p+bufpV9WuXOLa2qGiZCh2smHbsdnhzE6XyNUXLHF108NU11cyrvqu8bco1UvTCtsTIo5/S/JeFZ d15LNea9lcu681mtVouF6LdUqrK5ZrFey8KVtcTqF2mlcWKlxaW0lS/RdK12trS6XhQtLF4XlUnT eUlK7VLi7UKF5XE64vK910u1VXLJdFcqaLdVVlxp8u/nor+3d8X/KcI18XU471Wk9j5fP0fs6c5+ muL7Ge+XlSHLxNLNBuXblezY20hj0bE+rjo6Oz3FFZ68lNMwdlujuJS8424s7jfut3TaCk1jrB2+ MOTdV3x460PPn2i/prlrtsz1v8Hyftrr4v7sFd8dz1XSEocr9856rjb2VbXulvWhTs9u9odLShrf x3rvmU7Y3b1XjO+/lx4790+ueF1uN6eOkH1jCc+PXHdDh+hgz4V6+b6TabrTbze6M1bZF9Z+mfsc mt81moiI74dkb1WP43vptp7oXnc+IJF84XtPDVnSK3tgtmy1xKl53pik1HNaelHXja1Mnq129d1w xO/lEnRJPrU+rZLc+NeHeDm+t+oq8WMQc5cvm8zbBWiwvTL3VS7StWjPfcdid1mOxGF5R0zV6S4k rd7ud8+mvPt2b0tfy8/PR3Xjy8ecPTz8sc+z/Hx29W8PX08fLPHTz3bfztruuzet2l1Nu/fbl5dt eXrx3LKG1ufd2t4J14++K8VrP1kd1iyTd5t4W9ZSxXuolZuCL2J1tx565zrji8Zy6eVzo13116D3 fvff+lD63yrLPwfKOfp2u8aaOfw0m84M50/Pd3Pq/tIzVOn2Ov6rzwcsR35/l8fdtPoA+T43++1F h+Ogr66WealLONB8fT66+H3fB245Ht9Q59O3kf1n+hnHs8vb7g46f9Hx/DzjqM327nqe0OieUf/b 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 ChIbAOiuYA==