# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080711163832-cutvrahspirds0fw # target_branch: ../../repo/trunk-clean/ # testament_sha1: 25c6cfc2b182753473e3e2647dd4a223f17a28ec # timestamp: 2008-07-11 10:09:11 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080711163623-vpxrq241l75k5jed # # Begin patch === modified file '.bzrignore' --- .bzrignore 2008-07-03 04:15:16 +0000 +++ .bzrignore 2008-07-03 14:08:39 +0000 @@ -6,3 +6,5 @@ ./conf/powerpc-ieee1275.mk ./conf/sparc64-ieee1275.mk ./build* +./util/fonttool/build +./util/fonttool/fonttool.iws === modified file 'ChangeLog' --- ChangeLog 2008-07-07 14:11:06 +0000 +++ ChangeLog 2008-07-11 16:37:47 +0000 @@ -12,6 +12,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-02 18:03:23 +0000 +++ Makefile.in 2008-07-04 21:40:36 +0000 @@ -85,6 +85,39 @@ YACC = @YACC@ UNIFONT_HEX = @UNIFONT_HEX@ +### Pretty output control ### +# Set up compiler and linker commands that either is quiet (does not print +# the command line being executed) or verbose (print the command line). +_CC := $(CC) +_TARGET_CC := $(TARGET_CC) +_STRIP := $(STRIP) +_GENMODSRC := sh $(srcdir)/genmodsrc.sh +ifeq ($(V),1) + override V_PREFIX := + override CC = $(_CC) + override TARGET_CC = $(_CC) + override STRIP = $(_STRIP) + override GENMODSRC = $(_GENMODSRC) + override INFO_GENCMDLIST = + override INFO_GENFSLIST = + override INFO_GENPARTMAPLIST = + override INFO_GEN_FINAL_COMMAND_LIST = + override INFO_GEN_FINAL_FS_LIST = + override INFO_GEN_FINAL_PARTMAP_LIST = +else + override V_PREFIX := @ + override CC = @echo "COMPILE $<"; $(_CC) + override TARGET_CC = @echo "COMPILE(TARGET) $<"; $(_TARGET_CC) + override STRIP = @echo "STRIP $@"; $(_STRIP) + override GENMODSRC = @echo "GENMODSRC $@"; $(_GENMODSRC) + override INFO_GENCMDLIST = @echo "GENCMDLIST $@" + override INFO_GENFSLIST = @echo "GENFSLIST $@" + override INFO_GENPARTMAPLIST = @echo "GENPARTMAPLIST $@" + override INFO_GEN_FINAL_COMMAND_LIST = @echo "GENCMDLIST[final] $@" + override INFO_GEN_FINAL_FS_LIST = @echo "GENFSLIST[final] $@" + override INFO_GEN_FINAL_PARTMAP_LIST = @echo "GENPARTMAPLIST[final] $@" +endif + # Options. enable_grub_emu = @enable_grub_emu@ enable_grub_fstest = @enable_grub_fstest@ @@ -131,13 +164,16 @@ || (rm -f $@; exit 1) command.lst: $(COMMANDFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_COMMAND_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ fs.lst: $(FSFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_FS_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ partmap.lst: $(PARTMAPFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_PARTMAP_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ ifeq (, $(UNIFONT_HEX)) else === modified file 'commands/i386/pc/vbeinfo.c' --- commands/i386/pc/vbeinfo.c 2007-07-21 22:32:33 +0000 +++ commands/i386/pc/vbeinfo.c 2008-07-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-09 16:52:26 +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-02 06:20:11 +0000 +++ conf/common.rmk 2008-07-07 15:07:13 +0000 @@ -287,6 +287,7 @@ cmp.mod cat.mod help.mod font.mod search.mod \ loopback.mod fs_uuid.mod configfile.mod echo.mod \ terminfo.mod test.mod blocklist.mod hexdump.mod \ + protomenu.mod \ read.mod sleep.mod loadenv.mod # For hello.mod. @@ -330,7 +331,7 @@ help_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) @@ -374,6 +375,11 @@ hexdump_mod_CFLAGS = $(COMMON_CFLAGS) hexdump_mod_LDFLAGS = $(COMMON_LDFLAGS) +# For protomenu.mod. +protomenu_mod_SOURCES = protomenu/protomenu.c protomenu/widget-box.c +protomenu_mod_CFLAGS = $(COMMON_CFLAGS) +protomenu_mod_LDFLAGS = $(COMMON_LDFLAGS) + # For read.mod. read_mod_SOURCES = commands/read.c read_mod_CFLAGS = $(COMMON_CFLAGS) === modified file 'conf/i386-efi.rmk' --- conf/i386-efi.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-efi.rmk 2008-07-03 04:19:16 +0000 @@ -84,7 +84,9 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/efi/init.c kern/parser.c kern/partition.c \ kern/env.c symlist.c kern/efi/efi.c kern/efi/init.c kern/efi/mm.c \ - term/efi/console.c disk/efi/efidisk.c + term/efi/console.c disk/efi/efidisk.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c kernel_mod_HEADERS = arg.h boot.h cache.h device.h disk.h dl.h elf.h elfload.h \ env.h err.h file.h fs.h kernel.h loader.h misc.h mm.h net.h parser.h \ partition.h pc_partition.h rescue.h symbol.h term.h time.h types.h \ === modified file 'conf/i386-linuxbios.rmk' --- conf/i386-linuxbios.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-linuxbios.rmk 2008-07-04 16:55:48 +0000 @@ -18,6 +18,7 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/parser.c kern/partition.c \ kern/env.c \ + kern/generic/get_time_ms.c \ term/i386/pc/console.c \ term/i386/pc/at_keyboard.c term/i386/pc/vga_text.c \ symlist.c === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-pc.rmk 2008-07-10 13:55:34 +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) @@ -256,7 +260,10 @@ videotest_mod_LDFLAGS = $(COMMON_LDFLAGS) # For bitmap.mod -bitmap_mod_SOURCES = video/bitmap.c +bitmap_mod_SOURCES = video/bitmap.c \ + video/bitmap_scale_nn.c \ + video/bitmap_scale_bilinear.c \ + video/bitmap_scale.c bitmap_mod_CFLAGS = $(COMMON_CFLAGS) bitmap_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'conf/powerpc-ieee1275.rmk' --- conf/powerpc-ieee1275.rmk 2008-06-19 04:14:16 +0000 +++ conf/powerpc-ieee1275.rmk 2008-07-03 04:19:16 +0000 @@ -85,6 +85,7 @@ kern/ieee1275/init.c term/ieee1275/ofconsole.c \ kern/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/parser.c kern/partition.c kern/env.c kern/powerpc/dl.c \ + kern/generic/millisleep.c \ symlist.c kern/powerpc/cache.S kernel_elf_HEADERS = grub/powerpc/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) === modified file 'conf/sparc64-ieee1275.rmk' --- conf/sparc64-ieee1275.rmk 2008-06-19 00:04:59 +0000 +++ conf/sparc64-ieee1275.rmk 2008-07-03 14:16:34 +0000 @@ -73,6 +73,7 @@ kern/rescue.c kern/term.c term/ieee1275/ofconsole.c \ kern/sparc64/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/partition.c kern/env.c kern/sparc64/dl.c symlist.c \ + kern/generic/millisleep.c kern/generic/get_time_ms.c \ kern/sparc64/cache.S kern/parser.c kernel_elf_HEADERS = grub/sparc64/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) @@ -195,7 +196,7 @@ cat_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) === added file 'font/font.c' --- font/font.c 1970-01-01 00:00:00 +0000 +++ font/font.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,92 @@ +/* font.c - Font functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +grub_font_t +grub_font_get (const char *font_name) +{ + struct font_node *node; + + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + if (grub_strcmp (font->name, font_name) == 0) + return font; + } + + /* If no font by that name is found, return the first font in the list + * as a fallback. */ + return grub_font_list->value; +} + +const char * +grub_font_get_name (grub_font_t font) +{ + return font->name; +} + +int +grub_font_get_max_char_width (grub_font_t font) +{ + return font->max_char_width; +} + +int +grub_font_get_max_char_height (grub_font_t font) +{ + return font->max_char_height; +} + +int +grub_font_get_ascent (grub_font_t font) +{ + return font->ascent; +} + +int +grub_font_get_descent (grub_font_t font) +{ + return font->descent; +} + +int +grub_font_get_string_width (grub_font_t font, const char *str) +{ + int i; + int width; + struct grub_font_glyph *glyph; + grub_size_t len; + + len = grub_strlen (str); + + for (i = 0, width = 0; i < len; i++) + { + glyph = grub_font_get_glyph (font, str[i]); + width += glyph->device_width; + } + + return width; +} === added file 'font/font_cmd.c' --- font/font_cmd.c 1970-01-01 00:00:00 +0000 +++ font/font_cmd.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,75 @@ +/* font_cmd.c - Font command definition. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include + +static grub_err_t +loadfont_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc, + char **args) +{ + if (argc == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); + + while (argc--) + if (grub_font_load (*args++) != 0) + return GRUB_ERR_BAD_FONT; + + return GRUB_ERR_NONE; +} + +static grub_err_t +lsfonts_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + struct font_node *node; + + grub_printf ("Loaded fonts:\n"); + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + grub_printf ("%s\n", font->name); + } + + return GRUB_ERR_NONE; +} + +GRUB_MOD_INIT(font_manager) +{ + grub_font_loader_init (); + + grub_register_command ("loadfont", loadfont_command, GRUB_COMMAND_FLAG_BOTH, + "loadfont FILE...", + "Specify one or more font files to load.", 0); + + grub_register_command ("lsfonts", lsfonts_command, GRUB_COMMAND_FLAG_BOTH, + "lsfonts", + "List the loaded fonts.", 0); +} + +GRUB_MOD_FINI(font_manager) +{ + /* Should this free fonts, unknown_glyph, etc.? Freeing fonts could + * be a Bad Thing if there are still references to any of them. */ + + grub_unregister_command ("loadfont"); +} + === added file 'font/loader.c' --- font/loader.c 1970-01-01 00:00:00 +0000 +++ font/loader.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,696 @@ +/* loader.c - Functions to handle loading fonts and glyphs from files. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +//#include +//#include +#include +#include +#include + +/* Definition of font registry. */ +struct font_node *grub_font_list; + +static int +register_font (grub_font_t font); + +static void +free_font (grub_font_t font); + +static void +remove_font (grub_font_t font); + +struct font_file_section +{ + grub_file_t file; /* The file this section is in. */ + char name[4]; /* FOURCC name of the section. */ + grub_uint32_t length; /* Length of the section contents. */ + int eof; /* Set by open_section() on EOF. */ +}; + +/* Font file format constants. */ +static const char pff2_magic[4] = { 'P', 'F', 'F', '2' }; +static const char section_names_file[4] = { 'F', 'I', 'L', 'E' }; +static const char section_names_font_name[4] = { 'N', 'A', 'M', 'E' }; +static const char section_names_max_char_width[4] = { 'M', 'A', 'X', 'W' }; +static const char section_names_max_char_height[4] = { 'M', 'A', 'X', 'H' }; +static const char section_names_ascent[4] = { 'A', 'S', 'C', 'E' }; +static const char section_names_descent[4] = { 'D', 'E', 'S', 'C' }; +static const char section_names_char_index[4] = { 'C', 'H', 'I', 'X' }; +static const char section_names_data[4] = { 'D', 'A', 'T', 'A' }; + +/* Replace unknown glyphs with a rounded question mark. */ +static grub_uint8_t unknown_glyph_bitmap[] = +{ + /* 76543210 */ + 0x7C, /* ooooo */ + 0x82, /* o o */ + 0xBA, /* o ooo o */ + 0xAA, /* o o o o */ + 0xAA, /* o o o o */ + 0x8A, /* o o o */ + 0x9A, /* o oo o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x7C, /* ooooo */ + 0x00 /* */ +}; + +static struct grub_font_glyph *unknown_glyph; + +void +grub_font_loader_init (void) +{ + unknown_glyph = grub_malloc(sizeof(struct grub_font_glyph) + + sizeof(unknown_glyph_bitmap)); + if (! unknown_glyph) + return; + + unknown_glyph->width = 8; + unknown_glyph->height = 16; + unknown_glyph->offset_x = 0; + unknown_glyph->offset_y = 0; + unknown_glyph->device_width = 8; + grub_memcpy(unknown_glyph->bitmap, + unknown_glyph_bitmap, sizeof(unknown_glyph_bitmap)); +} + +/* + * Open the next section in the file. + * + * On success, the section name is stored in section->name and the length in + * section->length, and 0 is returned. On failure, 1 is returned and + * grub_errno is set approriately with an error message. + * + * If 1 is returned due to being at the end of the file, then section->eof is + * set to 1; otherwise, section->eof is set to 0. + */ +static int +open_section (grub_file_t file, struct font_file_section *section) +{ + grub_ssize_t retval; + grub_uint32_t raw_length; + + section->file = file; + section->eof = 0; + + /* Read the FOURCC section name. */ + retval = grub_file_read (file, section->name, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section name"); + return 1; /* Read error. */ + } + + /* Read the big-endian 32-bit section length. */ + retval = grub_file_read (file, (char *) &raw_length, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section length"); + return 1; /* Read error. */ + } + + /* Convert byte-order and store in *length. */ + section->length = grub_be_to_cpu32 (raw_length); + + return 0; +} + +/* Size in bytes of each character index (CHIX section) + * entry in the font file. */ +#define FONT_CHAR_INDEX_ENTRY_SIZE (4 + 1 + 4) + +/* + * Load the character index (CHIX) section contents from the font file. This + * presumes that the position of FILE is positioned immediately after the + * section length for the CHIX section (i.e., at the start of the section + * contents). Returns 0 upon success, nonzero for failure (in which case + * grub_errno is set appropriately). + */ +static int +load_font_index (grub_file_t file, grub_uint32_t sect_length, struct + grub_font *font) +{ + unsigned i; + +#if FONT_DEBUG >= 2 + grub_printf("load_font_index(sect_length=%d)\n", sect_length); +#endif + + /* Sanity check: ensure section length is divisible by the entry size. */ + if (sect_length % FONT_CHAR_INDEX_ENTRY_SIZE != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: character index length %d " + "is not a multiple of the entry size %d", + sect_length, FONT_CHAR_INDEX_ENTRY_SIZE); + return 1; /* Invalid index section length. */ + } + + /* Calculate the number of characters. */ + font->num_chars = sect_length / FONT_CHAR_INDEX_ENTRY_SIZE; + + /* Allocate the character index array. */ + font->char_index = grub_malloc (font->num_chars + * sizeof (struct char_index_entry)); + if (!font->char_index) + return 1; /* Error allocating memory. */ + +#if FONT_DEBUG >= 2 + grub_printf("num_chars=%d)\n", font->num_chars); +#endif + + /* Load the character index data from the file. */ + for (i = 0; i < font->num_chars; i++) + { + struct char_index_entry *entry = &font->char_index[i]; + + /* Read code point value; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->code, 4) != 4) + return 1; + entry->code = grub_be_to_cpu32 (entry->code); + + /* Read storage flags byte. */ + if (grub_file_read (file, (char *) &entry->storage_flags, 1) != 1) + return 1; + + /* Read glyph data offset; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->offset, 4) != 4) + return 1; + entry->offset = grub_be_to_cpu32 (entry->offset); + + /* No glyph loaded. Will be loaded on demand and cached thereafter. */ + entry->glyph = 0; + +#if FONT_DEBUG >= 5 + if (i < 10) /* Print the 1st 10 characters. */ + grub_printf("c=%d o=%d\n", entry->code, entry->offset); +#endif + } + + return 0; /* Index loaded OK. */ +} + +/* + * Read the contents of the specified section as a string, which is + * allocated on the heap. Returns 0 if there is an error. + */ +static char * +read_section_as_string (struct font_file_section *section) +{ + char *str; + grub_ssize_t ret; + + str = grub_malloc (section->length + 1); + if (!str) + return 0; + + ret = grub_file_read (section->file, str, section->length); + if (ret < 0 || ret != (grub_ssize_t) section->length) + { + grub_free (str); + return 0; + } + + str[section->length] = '\0'; + return str; +} + +/* + * Read the contents of the current section as a 16-bit integer value, + * which is stored into *VALUE. Returns 0 upon success, nonzero upon failure. + */ +static int +read_section_as_short (struct font_file_section *section, grub_int16_t *value) +{ + grub_uint16_t raw_value; + + if (section->length != 2) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: section %c%c%c%c length " + "is %d but should be 2", + section->name[0], section->name[1], + section->name[2], section->name[3], + section->length); + return 1; /* An error occurred. */ + } + if (grub_file_read (section->file, (char *) &raw_value, 2) != 2) + return 1; /* An error occurred. */ + + *value = grub_be_to_cpu16 (raw_value); + return 0; /* Successfully read the value. */ +} + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int +grub_font_load (const char *filename) +{ + grub_file_t file = 0; + struct font_file_section section; + char magic[4]; + grub_font_t font = 0; + +#if FONT_DEBUG >= 1 + grub_printf("add_font(%s)\n", filename); +#endif + + file = grub_file_open (filename); + if (!file) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("file opened\n"); +#endif + + /* Read the FILE section. It indicates the file format. */ + if (open_section (file, §ion) != 0) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("opened FILE section\n"); +#endif + if (grub_memcmp (section.name, section_names_file, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: 1st section must be FILE"); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section name ok\n"); +#endif + if (section.length != 4) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error (file type ID length is %d " + "but should be 4)", section.length); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section length ok\n"); +#endif + /* Check the file format type code. */ + if (grub_file_read (file, magic, 4) != 4) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("read magic ok\n"); +#endif + + if (grub_memcmp (magic, pff2_magic, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, "Invalid font magic %x %x %x %x", + magic[0], magic[1], magic[2], magic[3]); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("compare magic ok\n"); +#endif + + /* Allocate the font object. */ + font = (grub_font_t) grub_malloc (sizeof (struct grub_font)); + if (!font) + goto fail; + + font->file = file; + font->name = 0; + font->ascent = 0; + font->descent = 0; + font->max_char_width = 0; + font->max_char_height = 0; + font->num_chars = 0; + font->char_index = 0; + +#if FONT_DEBUG >= 3 + grub_printf("allocate font ok; loading font info\n"); +#endif + + /* Load the font information. */ + while (1) + { + if (open_section (file, §ion) != 0) + { + if (section.eof) + break; /* Done reading the font file. */ + else + goto fail; + } + +#if FONT_DEBUG >= 2 + grub_printf("opened section %c%c%c%c ok\n", + section.name[0], section.name[1], + section.name[2], section.name[3]); +#endif + + if (grub_memcmp (section.name, section_names_font_name, 4) == 0) + { + font->name = read_section_as_string (§ion); + if (!font->name) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_width, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_width) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_height, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_height) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_ascent, 4) == 0) + { + if (read_section_as_short (§ion, &font->ascent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_descent, 4) == 0) + { + if (read_section_as_short (§ion, &font->descent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_char_index, 4) == 0) + { + /* Load the font index. */ + if (load_font_index (file, section.length, font) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_data, 4) == 0) + { + /* When the DATA section marker is reached, we stop reading. */ + break; + } + else + { + /* Unhandled section type, simply skip past it. */ +#if FONT_DEBUG >= 3 + grub_printf("Unhandled section type, skipping.\n"); +#endif + if ((int) grub_file_seek (file, + grub_file_tell (file) + section.length) + == -1) + goto fail; + } + } + + if (!font->name) + { + grub_printf ("Note: Font has no name.\n"); + font->name = grub_strdup ("Unknown"); + } + +#if FONT_DEBUG >= 1 + grub_printf ("Loaded font `%s'.\n" + "Ascent=%d Descent=%d MaxW=%d MaxH=%d Number of characters=%d.\n", + font->name, + font->ascent, font->descent, + font->max_char_width, font->max_char_height, + font->num_chars); +#endif + + if (font->max_char_width == 0 + || font->max_char_height == 0 + || font->num_chars == 0 + || font->char_index == 0 + || font->ascent == 0 + || font->descent == 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Invalid font file: missing some required data."); + goto fail; + } + + /* Add the font to the global font registry. */ + if (register_font (font) != 0) + goto fail; + + return 0; /* Font loaded ok. */ + +fail: + free_font (font); + return 1; /* Failed to load font. */ +} + +/* + * Read a 16-bit big-endian integer from FILE, convert it to native byte + * order, and store it in *VALUE. + * Returns 0 on success, 1 on failure. + */ +static int +read_be_uint16 (grub_file_t file, grub_uint16_t * value) +{ + if (grub_file_read (file, (char *) value, 2) != 2) + return 1; + *value = grub_be_to_cpu16 (*value); + return 0; +} + +static int +read_be_int16 (grub_file_t file, grub_int16_t * value) +{ + /* For the signed integer version, use the same code as for unsigned. */ + return read_be_uint16 (file, (grub_uint16_t *) value); +} + +/* + * Return a pointer to the character index entry for the glyph corresponding to + * the codepoint CODE in the font FONT. If not found, return zero. + */ +static struct char_index_entry * +find_glyph (const grub_font_t font, grub_uint32_t code) +{ + grub_uint32_t i; + grub_uint32_t len = font->num_chars; + struct char_index_entry *table = font->char_index; + + /* Do a linear search. */ + for (i = 0; i < len; i++) + { + if (table[i].code == code) + return &table[i]; + } + + return 0; /* No entry found for code point CODE. */ +} + +static struct grub_font_glyph * +grub_font_get_glyph_internal (grub_font_t font, grub_uint32_t code) +{ + struct char_index_entry *index_entry; + + index_entry = find_glyph (font, code); + if (index_entry) + { + struct grub_font_glyph *glyph = 0; + grub_uint16_t width; + grub_uint16_t height; + grub_int16_t xoff; + grub_int16_t yoff; + grub_int16_t dwidth; + int len; + + if (index_entry->glyph) + return index_entry->glyph; /* Return cached glyph. */ + + /* Make sure we can find glyphs for error messages. Push active + error message to error stack and reset error message. */ + grub_error_push (); + + grub_file_seek (font->file, index_entry->offset); + + /* Read the glyph width, height, and baseline. */ + if (read_be_uint16(font->file, &width) != 0 + || read_be_uint16(font->file, &height) != 0 + || read_be_int16(font->file, &xoff) != 0 + || read_be_int16(font->file, &yoff) != 0 + || read_be_int16(font->file, &dwidth) != 0) + { + //remove_font (font); + return 0; + } + + len = (width * height + 7) / 8; + glyph = grub_malloc (sizeof (struct grub_font_glyph) + len); + if (! glyph) + { + //remove_font (font); + return 0; + } + + glyph->font = font; + glyph->width = width; + glyph->height = height; + glyph->offset_x = xoff; + glyph->offset_y = yoff; + glyph->device_width = dwidth; + + /* Don't try to read empty bitmaps (e.g., space characters). */ + if (len != 0) + { + if (grub_file_read (font->file, (char *) glyph->bitmap, len) != len) + { + //remove_font (font); + return 0; + } + } + + /* Restore old error message. */ + grub_error_pop (); + + /* Cache the glyph. */ + index_entry->glyph = glyph; + + return glyph; /* Glyph loaded ok. */ + } + + return 0; +} + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph * +grub_font_get_glyph (grub_font_t font, grub_uint32_t code) +{ + struct grub_font_glyph *glyph; + glyph = grub_font_get_glyph_internal (font, code); + if (glyph == 0) + glyph = unknown_glyph; + return glyph; +} + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph * +grub_font_get_glyph_any (grub_uint32_t code) +{ + struct font_node *node; + /* Keep track of next node, in case there's an I/O error in + * grub_font_get_glyph() and the font is removed from the list. */ + struct font_node *next; + + for (node = grub_font_list; node; node = next) + { + grub_font_t font; + struct grub_font_glyph *glyph; + + font = node->value; + next = node->next; + + glyph = grub_font_get_glyph_internal (font, code); + if (glyph) + return glyph; + } + + /* Uggh... Glyph was not found in any font. */ + return unknown_glyph; /* Failed to load glyph. */ +} + +/* + * Free the memory used by a font. + * This should not be called if the font has been made available to + * users (once it is added to the global font list), since there would + * be the possibility of a dangling pointer. + */ +static void +free_font (grub_font_t font) +{ + if (font) + { + if (font->file) + grub_file_close (font->file); + if (font->name) + grub_free (font->name); + if (font->char_index) + grub_free (font->char_index); + grub_free (font); + } +} + +/* + * Add FONT to the global font registry. + * Returns 0 upon success, nonzero on failure (the font was not registered). + */ +static int +register_font (grub_font_t font) +{ + struct font_node *node = 0; + + node = grub_malloc (sizeof (struct font_node)); + if (!node) + return 1; /* Error. */ + + node->value = font; + node->next = grub_font_list; + grub_font_list = node; + + return 0; /* Success. */ +} + +/* + * Remove the font from the global font list. We don't actually free the + * font's memory since users could be holding references to the font. + */ +static void +remove_font (grub_font_t font) +{ + struct font_node **nextp, *cur; + + for (nextp = &grub_font_list, cur = *nextp; + cur; + nextp = &cur->next, cur = cur->next) + { + if (cur->value == font) + { + *nextp = cur->next; + + /* Free the node, but not the font itself. */ + grub_free (cur); + + return; + } + } +} + === removed file 'font/manager.c' --- font/manager.c 2008-02-10 11:04:38 +0000 +++ font/manager.c 1970-01-01 00:00:00 +0000 @@ -1,282 +0,0 @@ -/* - * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2005,2006,2007 Free Software Foundation, Inc. - * - * GRUB is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * GRUB is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with GRUB. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -struct entry -{ - grub_uint32_t code; - grub_uint32_t offset; -}; - -struct font -{ - struct font *next; - grub_file_t file; - grub_uint32_t num; - struct entry table[0]; -}; - -static struct font *font_list; - -/* Fill unknown glyph's with rounded question mark. */ -static grub_uint8_t unknown_glyph[16] = -{ /* 76543210 */ - 0x7C, /* ooooo */ - 0x82, /* o o */ - 0xBA, /* o ooo o */ - 0xAA, /* o o o o */ - 0xAA, /* o o o o */ - 0x8A, /* o o o */ - 0x9A, /* o oo o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x7C, /* ooooo */ - 0x00 /* */ -}; - -static int -add_font (const char *filename) -{ - grub_file_t file = 0; - char magic[4]; - grub_uint32_t num, i; - struct font *font = 0; - - file = grub_file_open (filename); - if (! file) - goto fail; - - if (grub_file_read (file, magic, 4) != 4) - goto fail; - - if (grub_memcmp (magic, GRUB_FONT_MAGIC, 4) != 0) - { - grub_error (GRUB_ERR_BAD_FONT, "invalid font magic"); - goto fail; - } - - if (grub_file_read (file, (char *) &num, 4) != 4) - goto fail; - - num = grub_le_to_cpu32 (num); - font = (struct font *) grub_malloc (sizeof (struct font) - + sizeof (struct entry) * num); - if (! font) - goto fail; - - font->file = file; - font->num = num; - - for (i = 0; i < num; i++) - { - grub_uint32_t code, offset; - - if (grub_file_read (file, (char *) &code, 4) != 4) - goto fail; - - if (grub_file_read (file, (char *) &offset, 4) != 4) - goto fail; - - font->table[i].code = grub_le_to_cpu32 (code); - font->table[i].offset = grub_le_to_cpu32 (offset); - } - - font->next = font_list; - font_list = font; - - return 1; - - fail: - if (font) - grub_free (font); - - if (file) - grub_file_close (file); - - return 0; -} - -static void -remove_font (struct font *font) -{ - struct font **p, *q; - - for (p = &font_list, q = *p; q; p = &(q->next), q = q->next) - if (q == font) - { - *p = q->next; - - grub_file_close (font->file); - grub_free (font); - - break; - } -} - -/* Return the offset of the glyph corresponding to the codepoint CODE - in the font FONT. If no found, return zero. */ -static grub_uint32_t -find_glyph (const struct font *font, grub_uint32_t code) -{ - grub_uint32_t start = 0; - grub_uint32_t end = font->num - 1; - const struct entry *table = font->table; - - /* This shouldn't happen. */ - if (font->num == 0) - return 0; - - /* Do a binary search. */ - while (start <= end) - { - grub_uint32_t i = (start + end) / 2; - - if (table[i].code < code) - start = i + 1; - else if (table[i].code > code) - end = i - 1; - else - return table[i].offset; - } - - return 0; -} - -/* Set the glyph to something stupid. */ -static void -fill_with_default_glyph (grub_font_glyph_t glyph) -{ - unsigned i; - - /* Use pre-defined pattern to fill unknown glyphs. */ - for (i = 0; i < 16; i++) - glyph->bitmap[i] = unknown_glyph[i]; - - glyph->char_width = 1; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; -} - -/* Get a glyph corresponding to the codepoint CODE. Always fill glyph - information with something, even if no glyph is found. */ -int -grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph) -{ - struct font *font; - grub_uint8_t bitmap[32]; - - /* FIXME: It is necessary to cache glyphs! */ - - restart: - for (font = font_list; font; font = font->next) - { - grub_uint32_t offset; - - offset = find_glyph (font, code); - if (offset) - { - grub_uint32_t w; - int len; - - /* Make sure we can find glyphs for error messages. Push active - error message to error stack and reset error message. */ - grub_error_push (); - - grub_file_seek (font->file, offset); - if ((len = grub_file_read (font->file, (char *) &w, sizeof (w))) - != sizeof (w)) - { - remove_font (font); - goto restart; - } - - w = grub_le_to_cpu32 (w); - if (w != 1 && w != 2) - { - /* grub_error (GRUB_ERR_BAD_FONT, "invalid width"); */ - remove_font (font); - goto restart; - } - - if (grub_file_read (font->file, (char *) bitmap, w * 16) - != (grub_ssize_t) w * 16) - { - remove_font (font); - goto restart; - } - - /* Fill glyph with information. */ - grub_memcpy (glyph->bitmap, bitmap, w * 16); - - glyph->char_width = w; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; - - /* Restore old error message. */ - grub_error_pop (); - - return 1; - } - } - - /* Uggh... No font was found. */ - fill_with_default_glyph (glyph); - return 0; -} - -static grub_err_t -font_command (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ - if (argc == 0) - return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); - - while (argc--) - if (! add_font (*args++)) - return 1; - - return 0; -} - -GRUB_MOD_INIT(font_manager) -{ - grub_register_command ("font", font_command, GRUB_COMMAND_FLAG_BOTH, - "font FILE...", - "Specify one or more font files to display.", 0); -} - -GRUB_MOD_FINI(font_manager) -{ - grub_unregister_command ("font"); -} === modified file 'genmk.rb' --- genmk.rb 2008-07-02 18:03:23 +0000 +++ genmk.rb 2008-07-04 21:40:36 +0000 @@ -125,7 +125,7 @@ $(TARGET_CC) $(TARGET_CPPFLAGS) $(TARGET_CFLAGS) $(#{prefix}_CFLAGS) -c -o $@ $< #{mod_src}: moddep.lst genmodsrc.sh - sh $(srcdir)/genmodsrc.sh '#{mod_name}' $< > $@ || (rm -f $@; exit 1) + $(GENMODSRC) '#{mod_name}' $< > $@ || (rm -f $@; exit 1) ifneq ($(#{prefix}_EXPORTS),no) #{defsym}: #{pre_obj} @@ -157,18 +157,21 @@ PARTMAPFILES += #{partmap} #{command}: #{src} $(#{src}_DEPENDENCIES) gencmdlist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENCMDLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/gencmdlist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{fs}: #{src} $(#{src}_DEPENDENCIES) genfslist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENFSLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genfslist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{partmap}: #{src} $(#{src}_DEPENDENCIES) genpartmaplist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENPARTMAPLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genpartmaplist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) === added file 'include/grub/bitmap_scale.h' --- include/grub/bitmap_scale.h 1970-01-01 00:00:00 +0000 +++ include/grub/bitmap_scale.h 2008-07-03 14:27:43 +0000 @@ -0,0 +1,54 @@ +/* bitmap_scale.h - Bitmap scaling functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_BITMAP_SCALE_HEADER +#define GRUB_BITMAP_SCALE_HEADER 1 + +#include +#include +#include + +enum grub_video_bitmap_scale_method +{ + /* Choose the fastest interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST, + /* Choose the highest quality interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST, + /* Nearest neighbor interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST, + /* Bilinear interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR +}; + +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum + grub_video_bitmap_scale_method scale_method); + +#endif /* ! GRUB_BITMAP_SCALE_HEADER */ === modified file 'include/grub/font.h' --- include/grub/font.h 2007-07-21 22:32:33 +0000 +++ include/grub/font.h 2008-07-03 14:17:31 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2007 Free Software Foundation, Inc. + * Copyright (C) 2003,2007,2008 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,33 +21,85 @@ #include -#define GRUB_FONT_MAGIC "PPF\x7f" +/* Forward declaration of opaque structure grub_font. + * Users only pass struct grub_font pointers to the font module functions, + * and do not have knowledge of the structure contents. */ +struct grub_font; + +/* Font type used to access font functions. */ +typedef struct grub_font *grub_font_t; + struct grub_font_glyph { - /* Glyph width in pixels. */ - grub_uint8_t width; - - /* Glyph height in pixels. */ - grub_uint8_t height; - - /* Glyph width in characters. */ - grub_uint8_t char_width; - - /* Glyph baseline position in pixels (from up). */ - grub_uint8_t baseline; - - /* Glyph bitmap data array of bytes in ((width + 7) / 8) * height. - Bitmap is formulated by height scanlines, each scanline having - width number of pixels. Pixels are coded as bits, value 1 meaning - of opaque pixel and 0 is transparent. If width does not fit byte - boundary, it will be padded with 0 to make it fit. */ - grub_uint8_t bitmap[32]; + /* Reference to the font this glyph belongs to. */ + grub_font_t font; + + /* Glyph bitmap width in pixels. */ + grub_uint16_t width; + + /* Glyph bitmap height in pixels. */ + grub_uint16_t height; + + /* Glyph bitmap x offset in pixels. Add to screen coordinate. */ + grub_int16_t offset_x; + + /* Glyph bitmap y offset in pixels. Subtract from screen coordinate. */ + grub_int16_t offset_y; + + /* Number of pixels to advance to start the next character. */ + grub_uint16_t device_width; + + /* Row-major order, packed bits (no padding; rows can break within a byte). + * The length of the array is (width * height + 7) / 8. Within a + * byte, the most significant bit is the first (leftmost/uppermost) pixel. + * Pixels are coded as bits, value 1 meaning of opaque pixel and 0 is + * transparent. If the length of the array does not fit byte boundary, it + * will be padded with 0 bits to make it fit. */ + grub_uint8_t bitmap[0]; }; -typedef struct grub_font_glyph *grub_font_glyph_t; - -int grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph); + +/****** font/font.c ******/ + +/* Get the font that has the specified name. Font names are in the form + * "Family Name Bold Italic 14", where Bold and Italic are optional. + * If no font matches the name specified, the most recently loaded font + * is returned as a fallback. */ +grub_font_t grub_font_get (const char *font_name); + +const char *grub_font_get_name (grub_font_t font); + +int grub_font_get_max_char_width (grub_font_t font); + +int grub_font_get_max_char_height (grub_font_t font); + +int grub_font_get_ascent (grub_font_t font); + +int grub_font_get_descent (grub_font_t font); + +int grub_font_get_string_width (grub_font_t font, const char *str); + + +/****** font/loader.c ******/ + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int grub_font_load (const char *filename); + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph *grub_font_get_glyph (grub_font_t font, + grub_uint32_t code); + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph *grub_font_get_glyph_any (grub_uint32_t code); #endif /* ! GRUB_FONT_HEADER */ === added file 'include/grub/font_internal.h' --- include/grub/font_internal.h 1970-01-01 00:00:00 +0000 +++ include/grub/font_internal.h 2008-07-03 14:16:11 +0000 @@ -0,0 +1,71 @@ +/* font_internal.h - Font declarations for use internally by the font module. + * Users of the font module should not include this header. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_FONT_INTERNAL_HEADER +#define GRUB_FONT_INTERNAL_HEADER 1 + +#include +#include +#include +#include +#include +#include +#include + +#define FONT_DEBUG 0 + +struct char_index_entry +{ + grub_uint32_t code; + grub_uint8_t storage_flags; + grub_uint32_t offset; + struct grub_font_glyph *glyph; /* Glyph if loaded, or null. */ +}; + +struct grub_font +{ + char *name; + grub_file_t file; + short max_char_width; + short max_char_height; + short ascent; + short descent; + grub_uint32_t num_chars; + struct char_index_entry *char_index; +}; + +struct font_node +{ + struct font_node *next; + struct grub_font *value; +}; + +extern struct font_node *grub_font_list; + + +/****** loader.c ******/ + +/* Initialize the font loader module. */ +void +grub_font_loader_init (void); + + +#endif /* ! GRUB_FONT_INTERNAL_HEADER */ + === modified file 'include/grub/i386/pc/vbeblit.h' --- include/grub/i386/pc/vbeblit.h 2007-07-21 22:32:33 +0000 +++ include/grub/i386/pc/vbeblit.h 2008-07-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-10 13:55:34 +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-11 14:56:59 +0000 @@ -0,0 +1,48 @@ +/* + * 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-11 16:03:24 +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); === added file 'include/grub/proto_widgets.h' --- include/grub/proto_widgets.h 1970-01-01 00:00:00 +0000 +++ include/grub/proto_widgets.h 2008-07-08 13:59:48 +0000 @@ -0,0 +1,48 @@ +/* proto_widgets.h - Widgets for the graphical menu prototype. */ +/* + * 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_PROTO_WIDGETS_HEADER +#define GRUB_PROTO_WIDGETS_HEADER 1 + +#include + +typedef struct grub_protomenu_widgets_box +{ + /* The size of the content. */ + int width; + int height; + + struct grub_video_bitmap **raw_pixmaps; + struct grub_video_bitmap **scaled_pixmaps; + + void (*draw) (struct grub_protomenu_widgets_box *self, int x, int y); + void (*set_content_size) (struct grub_protomenu_widgets_box *self, + int width, int height); + int (*get_left_pad) (struct grub_protomenu_widgets_box *self); + int (*get_top_pad) (struct grub_protomenu_widgets_box *self); + void (*destroy) (struct grub_protomenu_widgets_box *self); +} +*grub_protomenu_widgets_box_t; + +grub_protomenu_widgets_box_t +grub_protomenu_widgets_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix); + +#endif /* ! GRUB_PROTO_WIDGETS_HEADER */ + === modified file 'include/grub/time.h' --- include/grub/time.h 2007-10-22 19:02:16 +0000 +++ include/grub/time.h 2008-07-03 04:19:16 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2007 Free Software Foundation, Inc. + * Copyright (C) 2007, 2008 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,12 +19,13 @@ #ifndef KERNEL_TIME_HEADER #define KERNEL_TIME_HEADER 1 +#include #include #include #include void EXPORT_FUNC(grub_millisleep) (grub_uint32_t ms); -void EXPORT_FUNC(grub_millisleep_generic) (grub_uint32_t ms); +grub_uint64_t EXPORT_FUNC(grub_get_time_ms) (void); static __inline void grub_sleep (grub_uint32_t s) === modified file 'include/grub/video.h' --- include/grub/video.h 2008-01-01 12:02:07 +0000 +++ include/grub/video.h 2008-07-07 15:07:13 +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, === 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) { === added file 'kern/menu_viewer.c' --- kern/menu_viewer.c 1970-01-01 00:00:00 +0000 +++ kern/menu_viewer.c 2008-07-11 14:56:59 +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-11 14:56:59 +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-11 16:03:24 +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,14 @@ grub_wait_after_message (); } } + + return GRUB_ERR_NONE; } + +struct grub_menu_viewer grub_normal_terminal_menu_viewer = +{ + .name = "terminal", + .show_menu = show_menu +}; + + === added directory 'protomenu' === added file 'protomenu/protomenu.c' --- protomenu/protomenu.c 1970-01-01 00:00:00 +0000 +++ protomenu/protomenu.c 2008-07-11 16:03:24 +0000 @@ -0,0 +1,417 @@ +/* protomenu.c - Prototype graphical menu: command 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct menu_item +{ + const char *name; + struct grub_video_bitmap *icon; + grub_menu_entry_t entry; +} +menu_item_t; + +static const int icon_width = 48; +static const int icon_height = 48; +static grub_video_rect_t screen; +static grub_font_t title_font; +static grub_font_t item_font; +static grub_font_t status_font; +static grub_video_color_t title_color; +static grub_video_color_t item_color; +static grub_video_color_t status_color; +static grub_video_color_t status_bg_color; +static struct grub_video_bitmap *background_image; +static const char title_text[] = "GRUB 2 Boot Menu"; +static int num_items; +static menu_item_t *items; +static int selected_item_index; +static grub_protomenu_widgets_box_t menu_box; +static grub_protomenu_widgets_box_t selected_item_box; + +static grub_err_t +menu_init (grub_menu_t menu) +{ + num_items = menu->size; + if (num_items < 1) + return grub_error (GRUB_ERR_MENU, "Empty menu"); + + items = grub_malloc (num_items * sizeof (*items)); + if (!items) + return grub_errno; + + int i; + grub_menu_entry_t cur; + for (i = 0, cur = menu->entry_list; i < num_items; i++, cur = cur->next) + { + items[i].name = cur->title; + items[i].icon = 0; /* TODO match w/ patterns to choose an icon */ + items[i].entry = cur; + } + + /* + items[0].name = "Ubuntu 8.10"; + grub_video_bitmap_load (&img, "/boot/images/icon-ubuntu.tga"); + if (grub_errno == GRUB_ERR_NONE) + { + grub_video_bitmap_create_scaled (&items[0].icon, icon_width, + icon_height, img, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno != GRUB_ERR_NONE) + return; + grub_video_bitmap_destroy (img); + } + */ + + return GRUB_ERR_NONE; +} + +static void +menu_destroy (void) +{ + if (items) + { + int i; + for (i = 0; i < num_items; i++) + { + if (items[i].icon) + { + grub_video_bitmap_destroy (items[i].icon); + items[i].icon = 0; + } + } + grub_free (items); + items = 0; + } +} + +static grub_err_t +style_init (void) +{ + if (!(title_font = grub_font_get ("Helvetica Bold 24")) + || !(item_font = grub_font_get ("Helvetica Bold 14")) + || !(status_font = grub_font_get ("Helvetica 10"))) + return grub_errno; + + title_color = grub_video_map_rgb (0, 0, 0); + item_color = grub_video_map_rgb (0, 0, 0); + status_color = grub_video_map_rgb (255, 255, 255); + status_bg_color = grub_video_map_rgba (0, 0, 0, 112); + + struct grub_video_bitmap *img; + grub_video_bitmap_load (&img, "/boot/images/bg-protomenu1.tga"); + if (grub_errno != GRUB_ERR_NONE) + return grub_errno; + + grub_video_bitmap_create_scaled (&background_image, screen.width, + screen.height, img, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno != GRUB_ERR_NONE) + return grub_errno; + grub_video_bitmap_destroy (img); + + menu_box = grub_protomenu_widgets_create_box ("/boot/images/menubox_", ".tga"); + if (menu_box == 0) + return grub_error (GRUB_ERR_MENU, "Unable to create menu box"); + + selected_item_box = grub_protomenu_widgets_create_box ("/boot/images/select_blue_", ".tga"); + if (selected_item_box == 0) + return grub_error (GRUB_ERR_MENU, "Unable to create selection box"); + + return GRUB_ERR_NONE; +} + +static void +style_destroy (void) +{ + if (menu_box) + { + menu_box->destroy(menu_box); + menu_box = 0; + } + if (selected_item_box) + { + selected_item_box->destroy(selected_item_box); + selected_item_box = 0; + } +} + +static void +draw_background (void) +{ + grub_video_blit_bitmap (background_image, GRUB_VIDEO_BLIT_REPLACE, + 0, 0, 0, 0, + grub_video_bitmap_get_width (background_image), + grub_video_bitmap_get_height (background_image)); +} + +static void +draw_menu (void) +{ + int boxpad = 14; + int icon_text_space = 4; + int item_vspace = 16; + + int ascent = grub_font_get_ascent (item_font); + int descent = grub_font_get_descent (item_font); + int item_height = 52; + + grub_video_rect_t r; + r.width = screen.width * 2 / 3; + /* Set the menu box height to fit the items. */ + r.height = (item_height * num_items + + item_vspace * (num_items - 1) + + 2 * boxpad); + r.x = (screen.width - r.width) / 2; + r.y = (screen.height - r.height) / 2; + menu_box->set_content_size (menu_box, r.width, r.height); + + int menu_box_left_pad = menu_box->get_left_pad (menu_box); + int menu_box_top_pad = menu_box->get_top_pad (menu_box); + menu_box->draw (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 == selected_item_index) + { + selected_item_box->set_content_size (selected_item_box, + r.width - 2 * boxpad, + item_height); + int leftpad = selected_item_box->get_left_pad (selected_item_box); + int toppad = selected_item_box->get_top_pad (selected_item_box); + selected_item_box->draw (selected_item_box, + item_left - leftpad, + item_top - toppad); + } + + if (items[i].icon) + grub_video_blit_bitmap (items[i].icon, GRUB_VIDEO_BLIT_BLEND, + item_left, + item_top + (item_height - icon_height) / 2, + 0, 0, icon_width, icon_height); + + grub_video_draw_string (items[i].name, item_font, item_color, + item_left + icon_width + icon_text_space, + (item_top + (item_height - (ascent + descent)) + / 2 + ascent)); + + item_top += item_height + item_vspace; + } +} + +static void +draw_title (void) +{ + /* Center the title. */ + int title_width = grub_font_get_string_width (title_font, title_text); + int x = (screen.width - title_width) / 2; + int y = 40 + grub_font_get_ascent (title_font); + grub_video_draw_string (title_text, title_font, title_color, x, y); +} + +static void +draw_status (void) +{ + int descent = grub_font_get_descent (status_font); + int ascent = grub_font_get_ascent (status_font); + int vpad = 5; + int textheight = descent + ascent + 1; + int h = 2 * vpad + 2 * textheight; + + grub_video_fill_rect (status_bg_color, 0, screen.height - h, + screen.width, screen.height - 1); + + int texty = screen.height - h + vpad + ascent; + grub_video_draw_string ("Select an item with the arrow keys and " + "press Enter to boot.", + status_font, status_color, 30, texty); + texty += textheight; + grub_video_draw_string ("Press: 'c' for command line; 't' to switch to " + "non-graphical menu.", + status_font, status_color, 30, texty); + +} + +static grub_err_t +set_graphics_mode (void) +{ + const char *nodoublebuf_str = grub_env_get ("protomenu_nodoublebuf"); + int doublebuf_flags = + (nodoublebuf_str && nodoublebuf_str[0] == 'y') + ? 0 + : GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + + if (grub_video_setup (1024, 768, GRUB_VIDEO_MODE_TYPE_RGB | doublebuf_flags) + != GRUB_ERR_NONE) + return grub_errno; + + return GRUB_ERR_NONE; +} + +static grub_err_t +set_text_mode (void) +{ + return grub_video_restore (); +} + +/* TODO set the 'terminal' type to console for now, here. + (until gfxterm use is integrated). */ +static grub_err_t +show_menu (grub_menu_t menu, int nested __attribute__ ((unused))) +{ + set_graphics_mode (); + + grub_error_push (); + grub_video_get_viewport ((unsigned *) &screen.x, + (unsigned *) &screen.y, + (unsigned *) &screen.width, + (unsigned *) &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), + screen.x, screen.y, screen.width, screen.height); + grub_video_swap_buffers (); + + if (menu_init (menu) != GRUB_ERR_NONE) + goto menufail; + + if (style_init () != GRUB_ERR_NONE) + goto stylefail; + + selected_item_index = 0; + int done = 0; + while (!done && !grub_menu_viewer_should_return ()) + { + draw_background (); + draw_menu (); + draw_title (); + draw_status (); + grub_video_swap_buffers (); + + int c = GRUB_TERM_ASCII_CHAR (grub_getkey ()); + if (c == 'j' || c == 14) + { + selected_item_index++; + if (selected_item_index >= num_items) + selected_item_index = 0; + } + else if (c == 'k' || c == 16) + { + selected_item_index--; + if (selected_item_index < 0) + selected_item_index = num_items - 1; + } + else if (c == '\r' || c == '\n' || c == 6) + { + if (selected_item_index >=0 && selected_item_index < num_items) + { + menu_item_t *item = &items[selected_item_index]; + if (item->entry != 0) + { + set_text_mode (); + grub_menu_execute_entry (item->entry); + if (grub_errno != GRUB_ERR_NONE) + grub_wait_after_message (); + if (set_graphics_mode () != GRUB_ERR_NONE) + done = 1; + } + } + } + else if (c == 'c') + { + set_text_mode (); + grub_cmdline_run (1); + if (grub_errno != GRUB_ERR_NONE) + grub_wait_after_message (); + if (set_graphics_mode () != GRUB_ERR_NONE) + done = 1; + } + 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; + } + } + + style_destroy (); +stylefail: + menu_destroy (); +menufail: + set_text_mode (); + + grub_print_error (); + + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_cmd_protomenu (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"); + + show_menu (menu, 1); + + return 0; +} + +static struct grub_menu_viewer menu_viewer = +{ + .name = "protomenu", + .show_menu = show_menu +}; + +GRUB_MOD_INIT (protomenu) +{ + (void) mod; /* To stop warning. */ + grub_register_command ("protomenu", + grub_cmd_protomenu, GRUB_COMMAND_FLAG_BOTH, + "protomenu", "Show graphical menu prototype", 0); + grub_menu_viewer_register (&menu_viewer); +} + +GRUB_MOD_FINI (protomenu) +{ + grub_unregister_command ("protomenu"); +} === added file 'protomenu/widget-box.c' --- protomenu/widget-box.c 1970-01-01 00:00:00 +0000 +++ protomenu/widget-box.c 2008-07-08 13:59:48 +0000 @@ -0,0 +1,245 @@ +/* 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 (struct grub_protomenu_widgets_box *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_protomenu_widgets_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_protomenu_widgets_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 (struct grub_protomenu_widgets_box *self) +{ + return grub_video_bitmap_get_width (self->raw_pixmaps[BOX_PIXMAP_W]); +} + +static int +get_top_pad (struct grub_protomenu_widgets_box *self) +{ + return grub_video_bitmap_get_height (self->raw_pixmaps[BOX_PIXMAP_N]); +} + +static void +destroy (struct grub_protomenu_widgets_box *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_protomenu_widgets_box_t +grub_protomenu_widgets_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix) +{ + char path[200]; + unsigned i; + grub_protomenu_widgets_box_t box; + + box = (grub_protomenu_widgets_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 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; +} === modified file 'term/gfxterm.c' --- term/gfxterm.c 2008-01-01 12:02:07 +0000 +++ term/gfxterm.c 2008-07-03 14:28:01 +0000 @@ -29,14 +29,12 @@ #include #include #include +#include #define DEFAULT_VIDEO_WIDTH 640 #define DEFAULT_VIDEO_HEIGHT 480 #define DEFAULT_VIDEO_FLAGS 0 -#define DEFAULT_CHAR_WIDTH 8 -#define DEFAULT_CHAR_HEIGHT 16 - #define DEFAULT_BORDER_WIDTH 10 #define DEFAULT_STANDARD_COLOR 0x07 @@ -91,6 +89,9 @@ unsigned int cursor_y; int cursor_state; + /* Font settings. */ + grub_font_t font; + /* Terminal color settings. */ grub_uint8_t standard_color_setting; grub_uint8_t normal_color_setting; @@ -170,18 +171,25 @@ static grub_err_t grub_virtual_screen_setup (unsigned int x, unsigned int y, - unsigned int width, unsigned int height) + unsigned int width, unsigned int height, + const char *font_name) { /* Free old virtual screen. */ grub_virtual_screen_free (); /* Initialize with default data. */ + virtual_screen.font = grub_font_get (font_name); + if (!virtual_screen.font) + return grub_error (GRUB_ERR_BAD_FONT, + "No font loaded."); virtual_screen.width = width; virtual_screen.height = height; virtual_screen.offset_x = x; virtual_screen.offset_y = y; - virtual_screen.char_width = DEFAULT_CHAR_WIDTH; - virtual_screen.char_height = DEFAULT_CHAR_HEIGHT; + virtual_screen.char_width = + grub_font_get_max_char_width (virtual_screen.font); + virtual_screen.char_height = + grub_font_get_max_char_height (virtual_screen.font); virtual_screen.cursor_x = 0; virtual_screen.cursor_y = 0; virtual_screen.cursor_state = 1; @@ -229,6 +237,7 @@ static grub_err_t grub_gfxterm_init (void) { + char *font_name; char *modevar; int width = DEFAULT_VIDEO_WIDTH; int height = DEFAULT_VIDEO_HEIGHT; @@ -236,6 +245,11 @@ int flags = DEFAULT_VIDEO_FLAGS; grub_video_color_t color; + /* Select the font to use. */ + font_name = grub_env_get ("gfxterm_font"); + if (!font_name) + font_name = ""; /* Allow fallback to any font. */ + /* Parse gfxmode environment variable if set. */ modevar = grub_env_get ("gfxmode"); if (modevar) @@ -475,7 +489,7 @@ /* Create virtual screen. */ if (grub_virtual_screen_setup (DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_WIDTH, - width, height) != GRUB_ERR_NONE) + width, height, font_name) != GRUB_ERR_NONE) { grub_video_restore (); return grub_errno; @@ -661,11 +675,12 @@ write_char (void) { struct grub_colored_char *p; - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; grub_video_color_t color; grub_video_color_t bgcolor; unsigned int x; unsigned int y; + int ascent; /* Find out active character. */ p = (virtual_screen.text_buffer @@ -675,7 +690,8 @@ p -= p->index; /* Get glyph for character. */ - grub_font_get_glyph (p->code, &glyph); + glyph = grub_font_get_glyph (virtual_screen.font, p->code); + ascent = grub_font_get_ascent (virtual_screen.font); color = p->fg_color; bgcolor = p->bg_color; @@ -685,13 +701,13 @@ /* Render glyph to text layer. */ grub_video_set_active_render_target (text_layer); - grub_video_fill_rect (bgcolor, x, y, glyph.width, glyph.height); - grub_video_blit_glyph (&glyph, color, x, y); + grub_video_fill_rect (bgcolor, x, y, glyph->width, glyph->height); + grub_video_blit_glyph (glyph, color, x, y + ascent); grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); /* Mark character to be drawn. */ dirty_region_add (virtual_screen.offset_x + x, virtual_screen.offset_y + y, - glyph.width, glyph.height); + glyph->width, glyph->height); } static void @@ -705,7 +721,8 @@ /* Determine cursor properties and position on text layer. */ x = virtual_screen.cursor_x * virtual_screen.char_width; - y = ((virtual_screen.cursor_y + 1) * virtual_screen.char_height) - 3; + y = (virtual_screen.cursor_y * virtual_screen.char_height + + grub_font_get_ascent (virtual_screen.font)); width = virtual_screen.char_width; height = 2; @@ -822,14 +839,18 @@ } else { - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; struct grub_colored_char *p; + unsigned char_width; /* Get properties of the character. */ - grub_font_get_glyph (c, &glyph); + glyph = grub_font_get_glyph (virtual_screen.font, c); + + /* TODO [CDB] Fix wide characters. Bi-width font? */ + char_width = 1; /* If we are about to exceed line length, wrap to next line. */ - if (virtual_screen.cursor_x + glyph.char_width > virtual_screen.columns) + if (virtual_screen.cursor_x + char_width > virtual_screen.columns) grub_putchar ('\n'); /* Find position on virtual screen, and fill information. */ @@ -839,18 +860,18 @@ p->code = c; p->fg_color = virtual_screen.fg_color; p->bg_color = virtual_screen.bg_color; - p->width = glyph.char_width - 1; + p->width = char_width - 1; p->index = 0; /* If we have large glyph, add fixup info. */ - if (glyph.char_width > 1) + if (char_width > 1) { unsigned i; - for (i = 1; i < glyph.char_width; i++) + for (i = 1; i < char_width; i++) { p[i].code = ' '; - p[i].width = glyph.char_width - 1; + p[i].width = char_width - 1; p[i].index = i; } } @@ -859,7 +880,7 @@ write_char (); /* Make sure we scroll screen when needed and wrap line correctly. */ - virtual_screen.cursor_x += glyph.char_width; + virtual_screen.cursor_x += char_width; if (virtual_screen.cursor_x >= virtual_screen.columns) { virtual_screen.cursor_x = 0; @@ -879,11 +900,15 @@ static grub_ssize_t grub_gfxterm_getcharwidth (grub_uint32_t c) { - struct grub_font_glyph glyph; - - grub_font_get_glyph (c, &glyph); - - return glyph.char_width; +#if 0 + struct grub_font_glyph *glyph; + + glyph = grub_font_get_glyph (c); + + return glyph->char_width; +#else + return 1; /* TODO [CDB] Fix wide characters. */ +#endif } static grub_uint16_t @@ -1014,8 +1039,18 @@ dirty_region_redraw (); } + +/* Option array indices. */ +#define BACKGROUND_CMD_ARGINDEX_MODE 0 + +static const struct grub_arg_option background_image_cmd_options[] = { + {"mode", 'm', 0, "Background image mode (`stretch', `normal').", 0, + ARG_TYPE_STRING}, + {0, 0, 0, 0, 0, 0} +}; + static grub_err_t -grub_gfxterm_background_image_cmd (struct grub_arg_list *state __attribute__ ((unused)), +grub_gfxterm_background_image_cmd (struct grub_arg_list *state, int argc, char **args) { @@ -1042,12 +1077,36 @@ if (grub_errno != GRUB_ERR_NONE) return grub_errno; + /* Determine if the bitmap should be scaled to fit the screen. */ + if (!state[BACKGROUND_CMD_ARGINDEX_MODE].set + || grub_strcmp (state[BACKGROUND_CMD_ARGINDEX_MODE].arg, + "stretch") == 0) + { + if (mode_info.width != grub_video_bitmap_get_width (bitmap) + || mode_info.height != grub_video_bitmap_get_height (bitmap)) + { + struct grub_video_bitmap *scaled_bitmap; + grub_video_bitmap_create_scaled (&scaled_bitmap, + mode_info.width, + mode_info.height, + bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno == GRUB_ERR_NONE) + { + /* Replace the original bitmap with the scaled one. */ + grub_video_bitmap_destroy (bitmap); + bitmap = scaled_bitmap; + } + } + } + + /* If bitmap was loaded correctly, display it. */ if (bitmap) { /* Determine bitmap dimensions. */ bitmap_width = grub_video_bitmap_get_width (bitmap); - bitmap_height = grub_video_bitmap_get_width (bitmap); + bitmap_height = grub_video_bitmap_get_height (bitmap); /* Mark whole screen as dirty. */ dirty_region_reset (); @@ -1092,7 +1151,7 @@ GRUB_COMMAND_FLAG_BOTH, "background_image", "Load background image for active terminal", - 0); + background_image_cmd_options); } GRUB_MOD_FINI(term_gfxterm) === modified file 'term/i386/pc/vesafb.c' --- term/i386/pc/vesafb.c 2007-12-30 08:52:06 +0000 +++ term/i386/pc/vesafb.c 2008-07-03 14:12:08 +0000 @@ -250,10 +250,11 @@ break; default: - return grub_font_get_glyph (code, bitmap, width); + return grub_font_get_glyph_any (code, bitmap, width); } } + /* TODO [CDB] This is wrong for the new font module. Should it be fixed? */ if (bitmap) grub_memcpy (bitmap, vga_font + code * virtual_screen.char_height, === modified file 'term/i386/pc/vga.c' --- term/i386/pc/vga.c 2008-01-21 15:48:27 +0000 +++ term/i386/pc/vga.c 2008-07-03 14:12:08 +0000 @@ -65,6 +65,7 @@ static struct colored_char text_buf[TEXT_WIDTH * TEXT_HEIGHT]; static unsigned char saved_map_mask; static int page = 0; +static grub_font_t font = 0; #define SEQUENCER_ADDR_PORT 0x3C4 #define SEQUENCER_DATA_PORT 0x3C5 @@ -161,6 +162,9 @@ saved_map_mask = get_map_mask (); set_map_mask (0x0f); set_start_address (PAGE_OFFSET (page)); + font = grub_font_get (""); /* Choose any font, for now. */ + if (!font) + return grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); return GRUB_ERR_NONE; } @@ -185,7 +189,7 @@ write_char (void) { struct colored_char *p = text_buf + xpos + ypos * TEXT_WIDTH; - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; unsigned char *mem_base; unsigned plane; @@ -194,7 +198,7 @@ p -= p->index; /* Get glyph for character. */ - grub_font_get_glyph (p->code, &glyph); + glyph = grub_font_get_glyph (font, p->code); for (plane = 0x01; plane <= 0x08; plane <<= 1) { @@ -210,17 +214,21 @@ { unsigned i; - for (i = 0; i < glyph.char_width && offset < 32; i++) + unsigned char_width = 1; /* TODO [CDB] Figure out wide characters. */ + /* TODO [CDB] Re-implement glyph drawing for vga module. */ +#if 0 + for (i = 0; i < char_width && offset < 32; i++) { unsigned char fg_mask, bg_mask; - fg_mask = (p->fg_color & plane) ? glyph.bitmap[offset] : 0; - bg_mask = (p->bg_color & plane) ? ~(glyph.bitmap[offset]) : 0; + fg_mask = (p->fg_color & plane) ? glyph->bitmap[offset] : 0; + bg_mask = (p->bg_color & plane) ? ~(glyph->bitmap[offset]) : 0; offset++; if (check_vga_mem (mem + i)) mem[i] = (fg_mask | bg_mask); } +#endif /* 0 */ } } @@ -320,36 +328,37 @@ } else { - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; struct colored_char *p; + unsigned char_width = 1; - grub_font_get_glyph(c, &glyph); + glyph = grub_font_get_glyph(font, c); - if (xpos + glyph.char_width > TEXT_WIDTH) + if (xpos + char_width > TEXT_WIDTH) grub_putchar ('\n'); p = text_buf + xpos + ypos * TEXT_WIDTH; p->code = c; p->fg_color = fg_color; p->bg_color = bg_color; - p->width = glyph.char_width - 1; + p->width = char_width - 1; p->index = 0; - if (glyph.char_width > 1) + if (char_width > 1) { unsigned i; - for (i = 1; i < glyph.char_width; i++) + for (i = 1; i < char_width; i++) { p[i].code = ' '; - p[i].width = glyph.char_width - 1; + p[i].width = char_width - 1; p[i].index = i; } } write_char (); - xpos += glyph.char_width; + xpos += char_width; if (xpos >= TEXT_WIDTH) { xpos = 0; @@ -381,11 +390,16 @@ static grub_ssize_t grub_vga_getcharwidth (grub_uint32_t c) { +#if 0 struct grub_font_glyph glyph; - grub_font_get_glyph (c, &glyph); + glyph = grub_font_get_glyph (c); return glyph.char_width; +#else + /* TODO [CDB] Glyph wide characters? vga? */ + return 1; +#endif } static grub_uint16_t === added directory 'util/fonttool' === added file 'util/fonttool/build.xml' --- util/fonttool/build.xml 1970-01-01 00:00:00 +0000 +++ util/fonttool/build.xml 2008-07-03 14:08:39 +0000 @@ -0,0 +1,23 @@ + + GRUB 2 font tool. + + + + + + + + + + + + + + + + + + + + === added file 'util/fonttool/fonttool.iml' --- util/fonttool/fonttool.iml 1970-01-01 00:00:00 +0000 +++ util/fonttool/fonttool.iml 2008-07-03 14:08:39 +0000 @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + === added file 'util/fonttool/fonttool.ipr' --- util/fonttool/fonttool.ipr 1970-01-01 00:00:00 +0000 +++ util/fonttool/fonttool.ipr 2008-07-03 14:08:39 +0000 @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + === added directory 'util/fonttool/src' === added directory 'util/fonttool/src/org' === added directory 'util/fonttool/src/org/gnu' === added directory 'util/fonttool/src/org/gnu/grub' === added directory 'util/fonttool/src/org/gnu/grub/fonttool' === added file 'util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java' --- util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,24 @@ +package org.gnu.grub.fonttool; + +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; + +public class BDFFont { + private Map glyphs = new HashMap(); + private int height = 0; + + void putGlyph(char ch, BufferedImage glyph) { + glyphs.put(ch, glyph); + if (glyph.getHeight() > height) + height = glyph.getHeight(); + } + + public BufferedImage getGlyph(char c) { + return glyphs.get(c); + } + + public int getHeight() { + return height; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java' --- util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,254 @@ +package org.gnu.grub.fonttool; + +import java.io.*; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BDFLoader { + private final BufferedReader in; + private final Font font; + private int maxCharWidth; + private int maxCharHeight; + + BDFLoader(BufferedReader in) { + this.in = in; + this.font = new Font(); + this.maxCharWidth = 0; + this.maxCharHeight = 0; + } + + public static boolean isBDFFile(String filename) { + DataInputStream in = null; + try { + in = new DataInputStream(new FileInputStream(filename)); + final String signature = "STARTFONT "; + byte[] b = new byte[signature.length()]; + in.readFully(b); + in.close(); + + String s = new String(b, "US-ASCII"); + return signature.equals(s); + } catch (IOException e) { + if (in != null) { + try { + in.close(); + } catch (IOException e1) { + // Ignore. + } + } + return false; + } + } + + public static Font loadFontResource(String resourceName) throws IOException { + InputStream in = BDFLoader.class.getClassLoader().getResourceAsStream(resourceName); + if (in == null) + throw new FileNotFoundException("Font resource " + resourceName + " not found"); + return loadFontFromStream(in); + } + + public static Font loadFontFile(String filename) throws IOException { + InputStream in = new FileInputStream(filename); + return loadFontFromStream(in); + } + + private static Font loadFontFromStream(InputStream inStream) throws IOException { + BufferedReader in; + try { + in = new BufferedReader(new InputStreamReader(inStream, "ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Encoding not supported: " + e.getMessage(), e); + } + return new BDFLoader(in).loadFont(); + } + + private Font loadFont() throws IOException { + loadFontInfo(); + while (loadChar()) { + /* Loop. */ + } + + font.setMaxCharWidth(maxCharWidth); + font.setMaxCharHeight(maxCharHeight); + return font; + } + + private void loadFontInfo() throws IOException { + final Pattern stringSettingPattern = Pattern.compile("^(\\w+)\\s+\"([^\"]+)\"$"); + String line; + // Load the global font information that appears before CHARS. + final int UNSET = Integer.MIN_VALUE; + font.setAscent(UNSET); + font.setDescent(UNSET); + font.setFamily("Unknown"); + font.setBold(false); + font.setItalic(false); + font.setPointSize(Font.UNKNOWN_POINT_SIZE); + do { + line = in.readLine(); + if (line == null) + throw new IOException("BDF format error: end of file while " + + "reading global font information"); + + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens()) { + String name = st.nextToken(); + if (name.equals("FONT_ASCENT")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + font.setAscent(Integer.parseInt(st.nextToken())); + } else if (name.equals("FONT_DESCENT")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + font.setDescent(Integer.parseInt(st.nextToken())); + } else if (name.equals("POINT_SIZE")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + // Divide by 10, since it is stored X10. + font.setPointSize(Integer.parseInt(st.nextToken()) / 10); + } else if (name.equals("FAMILY_NAME")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + font.setFamily(matcher.group(2)); + } else if (name.equals("WEIGHT_NAME")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + String weightName = matcher.group(2); + font.setBold("bold".equalsIgnoreCase(weightName)); + } else if (name.equals("SLANT")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + String slantType = matcher.group(2); + font.setItalic(!"R".equalsIgnoreCase(slantType)); + } else if (name.equals("CHARS")) { + // This is the end of the global font information and + // the beginning of the character definitions. + break; + } else { + // Skip other fields. + } + } + } while (true); + + if (font.getAscent() == UNSET) + throw new IOException("BDF format error: no FONT_ASCENT property"); + if (font.getDescent() == UNSET) + throw new IOException("BDF format error: no FONT_DESCENT property"); + } + + private boolean loadChar() throws IOException { + String line; + // Find start of character + do { + line = in.readLine(); + if (line == null) + return false; + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens() && st.nextToken().equals("STARTCHAR")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no character name after STARTCHAR"); + break; + } + } while (true); + + // Find properties + final int UNSET = Integer.MIN_VALUE; + int codePoint = UNSET; + int bbx = UNSET; + int bby = UNSET; + int bbox = UNSET; + int bboy = UNSET; + int dwidth = UNSET; + do { + line = in.readLine(); + if (line == null) + return false; + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens()) { + String field = st.nextToken(); + if (field.equals("ENCODING")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no encoding # after ENCODING"); + String codePointStr = st.nextToken(); + codePoint = Integer.parseInt(codePointStr); + } else if (field.equals("BBX")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no tokens after BBX"); + bbx = Integer.parseInt(st.nextToken()); + bby = Integer.parseInt(st.nextToken()); + bbox = Integer.parseInt(st.nextToken()); + bboy = Integer.parseInt(st.nextToken()); + } else if (field.equals("DWIDTH")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no tokens after DWIDTH"); + dwidth = Integer.parseInt(st.nextToken()); + int dwidthY = Integer.parseInt(st.nextToken()); + // The DWIDTH Y value should be zero for any normal font. + if (dwidthY != 0) { + throw new IOException("BDF format error: dwidth Y value" + + "is nonzero (" + dwidthY + ") " + + "for char " + codePoint + "."); + } + } else if (field.equals("BITMAP")) { + break; // now read the bitmap + } + } + } while (true); + + if (codePoint == UNSET) + throw new IOException("BDF format error: " + + "no code point set"); + if (bbx == UNSET || bby == UNSET) + throw new IOException("BDF format error: " + + "bbx/bby missing: " + bbx + ", " + bby + + " for char " + codePoint); + + if (bbox == UNSET || bboy == UNSET) + throw new IOException("BDF format error: " + + "bbox/bboy missing: " + bbox + ", " + bboy + + " for char " + codePoint); + + if (dwidth == UNSET) + throw new IOException("BDF format error: " + + "dwidth missing for char " + codePoint); + + final int glyphWidth = bbx; + final int glyphHeight = bby; + if (glyphWidth > maxCharWidth) + maxCharWidth = glyphWidth; + if (glyphHeight > maxCharHeight) + maxCharHeight = glyphHeight; + + // Read the bitmap + Glyph glyph = new Glyph(codePoint, glyphWidth, glyphHeight, bbox, bboy, dwidth); + for (int y = 0; y < glyphHeight; y++) { + line = in.readLine(); + if (line == null) + return false; + for (int b = 0; b < line.length(); b++) { + int v = Integer.parseInt(Character.toString(line.charAt(b)), 16); + for (int x = b * 4, i = 0; i < 4 && x < glyphWidth; x++, i++) { + boolean set = (v & 0x8) != 0; + v <<= 1; + glyph.setPixel(x, y, set); + } + } + } + + font.putGlyph(codePoint, glyph); + return true; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,135 @@ +package org.gnu.grub.fonttool; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.TreeMap; + +class CharDefs { + private boolean debug = "1".equals(System.getProperty("fonttool.debug")); + private TreeMap glyphs; + private ByteArrayOutputStream charDefsData; + private int maxCharWidth; + private int maxCharHeight; + private HashMap charIndex; + + public CharDefs(Font font) { + this.glyphs = font.getGlyphs(); + this.charIndex = null; + this.charDefsData = null; + + calculateMaxSizes(); + } + + private void calculateMaxSizes() { + maxCharWidth = 0; + maxCharHeight = 0; + for (Glyph glyph : glyphs.values()) { + final int w = glyph.getWidth(); + final int h = glyph.getHeight(); + if (w > maxCharWidth) + maxCharWidth = w; + if (h > maxCharHeight) + maxCharHeight = h; + } + } + + void buildDefinitions() { + charIndex = new HashMap(); + HashMap charDefIndex = new HashMap(); + charDefsData = new ByteArrayOutputStream(); + DataOutputStream charDefs = new DataOutputStream(charDefsData); + try { + // Loop through all the glyphs, writing the glyph data to the + // in-memory byte stream, collapsing duplicate glyphs, and + // constructing index information. + for (Glyph glyph : glyphs.values()) { + CharDef charDef = new CharDef(glyph.getWidth(), + glyph.getHeight(), + glyph.getBitmap()); + + if (charDefIndex.containsKey(charDef)) { + // Use already-written glyph. + if (debug) + System.out.printf("Duplicate glyph for character U+%04X%n", + glyph.getCodePoint()); + final int charOffset = charDefIndex.get(charDef).intValue(); + final CharStorageInfo info = + new CharStorageInfo(glyph.getCodePoint(), charOffset); + charIndex.put(glyph.getCodePoint(), info); + } else { + // Write glyph data. + final int charOffset = charDefs.size(); + final CharStorageInfo info = + new CharStorageInfo(glyph.getCodePoint(), charOffset); + charIndex.put(glyph.getCodePoint(), info); + + charDefIndex.put(charDef, (long) charOffset); + + charDefs.writeShort(glyph.getWidth()); + charDefs.writeShort(glyph.getHeight()); + charDefs.writeShort(glyph.getBbox()); + charDefs.writeShort(glyph.getBboy()); + charDefs.writeShort(glyph.getDeviceWidth()); + charDefs.write(glyph.getBitmap()); + } + } + } catch (IOException e) { + throw new RuntimeException("Error writing to in-memory byte stream", e); + } + } + + public int getMaxCharWidth() { + return maxCharWidth; + } + + public int getMaxCharHeight() { + return maxCharHeight; + } + + public HashMap getCharIndex() { + if (charIndex == null) throw new IllegalStateException(); + return charIndex; + } + + public byte[] getDefinitionData() { + if (charDefsData == null) throw new IllegalStateException(); + return charDefsData.toByteArray(); + } + + + private static class CharDef { + private final int width; + private final int height; + private final byte[] data; + + public CharDef(int width, int height, byte[] data) { + this.width = width; + this.height = height; + this.data = data; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CharDef charDef = (CharDef) o; + + if (height != charDef.height) return false; + if (width != charDef.width) return false; + if (!Arrays.equals(data, charDef.data)) return false; + + return true; + } + + public int hashCode() { + int result; + result = width; + result = 31 * result + height; + result = 31 * result + Arrays.hashCode(data); + return result; + } + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,139 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.HashMap; +import java.util.Map; +import javax.swing.*; + +public class CharMatrix extends JComponent { + private Font charFont; + private int zoom = 1; + private int startingChar; + private Map glyphLocations = new HashMap(); + private GlyphSelectionListener glyphSelectionListener; + private Point mouseLocation; + + public interface GlyphSelectionListener { + void glyphSelected(int codePoint); + } + + public CharMatrix() { + addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + int codePoint = getCodePointForGlyphAt(e.getPoint()); + if (codePoint != -1) { + if (glyphSelectionListener != null) + glyphSelectionListener.glyphSelected(codePoint); + } + } + + public void mouseExited(MouseEvent e) { + mouseLocation = null; + repaint(); + } + }); + + addMouseMotionListener(new MouseMotionAdapter() { + public void mouseMoved(MouseEvent e) { + mouseLocation = e.getPoint(); + repaint(); + } + }); + } + + public void setGlyphSelectionListener(GlyphSelectionListener listener) { + this.glyphSelectionListener = listener; + } + + public Font getCharFont() { + return charFont; + } + + public void setCharFont(Font font) { + this.charFont = font; + repaint(); + } + + public int getZoom() { + return zoom; + } + + public void setZoom(int zoom) { + this.zoom = zoom; + repaint(); + } + + public int getStartingChar() { + return startingChar; + } + + public void setStartingChar(int startingChar) { + this.startingChar = startingChar; + repaint(); + } + + public int getCodePointForGlyphAt(Point p) { + for (Rectangle rectangle : glyphLocations.keySet()) { + if (rectangle.contains(p)) + return glyphLocations.get(rectangle); + } + return -1; + } + + protected void paintComponent(Graphics g) { + glyphLocations.clear(); + + if (charFont == null) + return; + + int w = getWidth(); + int h = getHeight(); + int maxCharWidth = charFont.getMaxCharWidth(); + int maxCharHeight = charFont.getMaxCharHeight(); + int space = 2; + int charsPerRow = w / (maxCharWidth + space); + int rows = h / (maxCharHeight + space); + + Rectangle highlightBounds = null; + int codePoint = startingChar; + int y = 3 + charFont.getAscent(); // Point y to the baseline. + for (int row = 0; row < rows; row++) { + int x = 3; + for (int col = 0; col < charsPerRow; col++) { + Glyph glyph = charFont.getGlyph(codePoint); + + if (glyph != null) { + Rectangle charBounds = + CharView.drawGlyph(g, charFont, glyph, x, y, zoom, getForeground()); + // Expand the bounding box for a bigger clickable area. + final int boundsPad = 1; + charBounds.x -= boundsPad; + charBounds.y -= boundsPad; + charBounds.width += 2 * boundsPad; + charBounds.height += 2 * boundsPad; + glyphLocations.put(charBounds, codePoint); + if (mouseLocation != null && charBounds.contains(mouseLocation)) { + highlightBounds = charBounds; + } + } + x += maxCharWidth + space; + codePoint++; + } + + y += maxCharHeight + space; + } + + if (highlightBounds != null) { + g.setColor(Color.blue); + Graphics2D g2 = (Graphics2D) g; + g2.setStroke(new BasicStroke(3f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 2f)); + g2.draw(new Rectangle(highlightBounds.x - 2, highlightBounds.y - 2, + highlightBounds.width + 3, highlightBounds.height + 3)); + g2.setStroke(new BasicStroke(1f)); + } + + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,19 @@ +package org.gnu.grub.fonttool; + +class CharStorageInfo { + private final int codePoint; + private final int fileOffset; + + public CharStorageInfo(int codePoint, int fileOffset) { + this.codePoint = codePoint; + this.fileOffset = fileOffset; + } + + public int getCodePoint() { + return codePoint; + } + + public int getFileOffset() { + return fileOffset; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharView.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharView.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharView.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,101 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import java.util.Formatter; +import javax.swing.*; + +public class CharView extends JComponent { + private Font charFont = null; + private Glyph glyph = null; + private int zoom = 1; + + public Font getCharFont() { + return charFont; + } + + public void setCharFont(Font charFont) { + this.charFont = charFont; + } + + public Glyph getGlyph() { + return glyph; + } + + public void setGlyph(Glyph glyph) { + this.glyph = glyph; + repaint(); + } + + public void setZoom(int zoom) { + this.zoom = zoom; + } + + public Dimension getPreferredSize() { + if (glyph == null) + return new Dimension(1, 1); + return new Dimension(glyph.getWidth() * zoom, glyph.getHeight() * zoom); + } + + protected void paintComponent(Graphics g) { + if (glyph == null) { + // Draw a gray 'X' indicating no character selected. + g.setColor(Color.gray); + g.drawLine(0, 0, getWidth() - 1, getHeight() - 1); + g.drawLine(getWidth(), 0, 0, getHeight() - 1); + return; + } + + final int x0 = (getWidth() - glyph.getWidth() * zoom) / 2; + final int y0 = getHeight() / 2; + drawGlyph(g, charFont, glyph, x0, y0, zoom, getForeground()); + + g.setFont(new java.awt.Font("SansSerif", java.awt.Font.PLAIN, 12)); + FontMetrics fm = g.getFontMetrics(); + Formatter f = new Formatter(); + f.format("U+%04X", glyph.getCodePoint()); + String str = f.toString(); + g.drawString(str, + (getWidth() - fm.stringWidth(str)) / 2, + getHeight() - fm.getHeight()); + } + + /** + * Draw the specified glyph with its baseline at y0 and the horizontal + * start at x0. + */ + static Rectangle drawGlyph(Graphics g, Font font, Glyph glyph, int startX, int baselineY, int zoom, Color fg) { + // Adjust (x0, y0) so that it puts the bitmap at the right location. + final int offsetX = zoom * glyph.getBbox(); + final int offsetY = zoom * glyph.getBboy(); + final int bitmapLeft = startX + offsetX; + final int bitmapBottom = baselineY - offsetY; + final int bitmapTop = bitmapBottom - zoom * glyph.getHeight(); + + // Draw a gray box around the glyph bounds. + g.setColor(Color.lightGray); + g.drawRect(bitmapLeft - 1, bitmapTop - 1, + glyph.getWidth() * zoom + 1, zoom * glyph.getHeight() + 1); + + // Draw the baseline + g.setColor(new Color(110, 110, 255)); + g.drawLine(startX, baselineY, startX + zoom * glyph.getDeviceWidth(), baselineY); + + // Draw the glyph. + g.setColor(fg); + byte[] bitmap = glyph.getBitmap(); + int bitIndex = 0; + for (int y = 0; y < glyph.getHeight(); y++) { + for (int x = 0; x < glyph.getWidth(); x++, bitIndex++) { + int byteIndex = bitIndex / 8; + int bitShift = bitIndex % 8; + byte b = bitmap[byteIndex]; + boolean bitValue = (b & (0x80 >>> bitShift)) != 0; + if (bitValue) { + g.fillRect(bitmapLeft + x * zoom, bitmapTop + y * zoom, zoom, zoom); + } + } + } + + return new Rectangle(bitmapLeft, bitmapTop, zoom * glyph.getWidth(), zoom * glyph.getHeight()); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Converter.java' --- util/fonttool/src/org/gnu/grub/fonttool/Converter.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Converter.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,78 @@ +package org.gnu.grub.fonttool; + +import java.io.IOException; + +/** + * Program to convert BDF fonts into PFF2 fonts for use with GRUB. + */ +public class Converter { + public static void main(String[] args) { + if (args.length < 1) { + printUsageAndExit(); + } + + String in = null; + String out = null; + + try { + for (String arg : args) { + if (arg.startsWith("--")) { + String option; + String value; + int equalsPos = arg.indexOf('='); + if (equalsPos < 0) { + option = arg.substring(2); + value = null; + } else { + option = arg.substring(2, equalsPos); + value = arg.substring(equalsPos + 1); + } + + if ("in".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + in = value; + } else if ("out".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + out = value; + } + } else { + throw new CommandLineException("Non-option argument `" + arg + "'."); + } + } + if (in == null || out == null) { + throw new CommandLineException("Both --in=X and --out=Y must be specified."); + } + } catch (CommandLineException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + + try { + // Read BDF. + Font font = BDFLoader.loadFontFile(in); + + // Write PFF2. + new PFF2Writer(out).writeFont(font); + } catch (IOException e) { + System.err.println("I/O error converting font: " + e); + e.printStackTrace(); + System.exit(1); + } + } + + private static class CommandLineException extends Exception { + public CommandLineException(String message) { + super(message); + } + } + + private static void printUsageAndExit() { + System.err.println("GNU GRUB Font Conversion Tool"); + System.err.println(); + System.err.println("Usage: Converter --in=IN.bdf --out=OUT.pf2"); + System.err.println(); + System.exit(1); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java' --- util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,9 @@ +package org.gnu.grub.fonttool; + +import java.io.IOException; + +public class FileFormatException extends IOException { + public FileFormatException(String msg) { + super(msg); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Font.java' --- util/fonttool/src/org/gnu/grub/fonttool/Font.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Font.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,106 @@ +package org.gnu.grub.fonttool; + +import java.util.TreeMap; + +public class Font { + public static final int UNKNOWN_POINT_SIZE = -1; + + private TreeMap glyphs; + private String family; + private boolean bold; + private boolean italic; + private int pointSize; + private int maxCharWidth; + private int maxCharHeight; + private int ascent; + private int descent; + + public Font() { + glyphs = new TreeMap(); + } + + public String getFamily() { + return family; + } + + public void setFamily(String family) { + this.family = family; + } + + public boolean isBold() { + return bold; + } + + public void setBold(boolean bold) { + this.bold = bold; + } + + public boolean isItalic() { + return italic; + } + + public void setItalic(boolean italic) { + this.italic = italic; + } + + public int getPointSize() { + return pointSize; + } + + public void setPointSize(int pointSize) { + this.pointSize = pointSize; + } + + public int getMaxCharWidth() { + return maxCharWidth; + } + + public void setMaxCharWidth(int maxCharWidth) { + this.maxCharWidth = maxCharWidth; + } + + public int getMaxCharHeight() { + return maxCharHeight; + } + + public void setMaxCharHeight(int maxCharHeight) { + this.maxCharHeight = maxCharHeight; + } + + public int getAscent() { + return ascent; + } + + public void setAscent(int ascent) { + this.ascent = ascent; + } + + public int getDescent() { + return descent; + } + + public void setDescent(int descent) { + this.descent = descent; + } + + public void putGlyph(int codePoint, Glyph glyph) { + glyphs.put(codePoint, glyph); + } + + public TreeMap getGlyphs() { + return glyphs; + } + + public Glyph getGlyph(int codePoint) { + return glyphs.get(codePoint); + } + + public String getStandardName() { + StringBuilder name = new StringBuilder(getFamily()); + if (isBold()) name.append(" Bold"); + if (isItalic()) name.append(" Italic"); + name.append(' '); + name.append(getPointSize()); + return name.toString(); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Glyph.java' --- util/fonttool/src/org/gnu/grub/fonttool/Glyph.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Glyph.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,83 @@ +package org.gnu.grub.fonttool; + +public class Glyph { + private final int codePoint; + private final int width; + private final int height; + + // These define the amounts to shift the character bitmap by + // before drawing it. See + // http://www.adobe.com/devnet/font/pdfs/5005.BDF_Spec.pdf + // and + // http://www.linuxbabble.com/documentation/x/bdf/ + // for explanatory figures. + private final int bbox; + private final int bboy; + + // Number of pixels to advance horizontally from this character's origin + // to the origin of the next character. + private final int deviceWidth; + + // Row-major order, no padding. Rows can break within a byte. + // MSb is first (leftmost/uppermost) pixel. + private final byte[] bitmap; + + public Glyph(int codePoint, int width, int height, int bbox, int bboy, int deviceWidth) { + this(codePoint, width, height, bbox, bboy, deviceWidth, + new byte[(width * height + 7) / 8]); + } + + public Glyph(int codePoint, int width, int height, + int bbox, int bboy, int deviceWidth, + byte[] bitmap) { + this.codePoint = codePoint; + this.width = width; + this.height = height; + this.bboy = bboy; + this.bbox = bbox; + this.deviceWidth = deviceWidth; + this.bitmap = bitmap; + } + + public void setPixel(int x, int y, boolean value) { + if (x < 0 || y < 0 || x >= width || y >= height) + throw new IllegalArgumentException( + "Invalid pixel location (" + x + ", " + y + ") for " + + width + "x" + height + " glyph"); + + int bitIndex = y * width + x; + int byteIndex = bitIndex / 8; + int bitPos = bitIndex % 8; + int v = value ? 0x80 >>> bitPos : 0; + int mask = ~(0x80 >>> bitPos); + bitmap[byteIndex] = (byte) ((bitmap[byteIndex] & mask) | v); + } + + public int getCodePoint() { + return codePoint; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getBbox() { + return bbox; + } + + public int getBboy() { + return bboy; + } + + public int getDeviceWidth() { + return deviceWidth; + } + + public byte[] getBitmap() { + return bitmap; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,203 @@ +package org.gnu.grub.fonttool; + +import java.io.EOFException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; + +/** + * Load a PFF2 font completely into memory as an instance of Font. + */ +public class PFF2Loader { + private static final int debug + = Integer.parseInt(System.getProperty("fonttool.debug", "0")); + + private RandomAccessFile f; + private Font font; + + private PFF2Loader(String filename) throws FileNotFoundException { + f = new RandomAccessFile(filename, "r"); + font = new Font(); + } + + public static boolean isPFF2File(String filename) { + RandomAccessFile f = null; + boolean isFont = false; + try { + f = new RandomAccessFile(filename, "r"); + byte[] sectionNameBytes = new byte[4]; + f.readFully(sectionNameBytes); + String sectionName = new String(sectionNameBytes, "US-ASCII"); + if (sectionName.equals(PFF2Sections.FILE)) { + final String expectedType = "PFF2"; + int len = f.readInt(); + if (len == expectedType.length()) { + byte[] typeBytes = new byte[len]; + f.readFully(typeBytes); + String type = new String(typeBytes, "US-ASCII"); + if (type.equals(expectedType)) { + isFont = true; + } + } + } + f.close(); + } catch (IOException e) { + if (f != null) { + try { + f.close(); + } catch (IOException e1) { + // Ignore. + } + } + } + + return isFont; + } + + public static Font loadFontFile(String filename) throws IOException { + PFF2Loader loader = new PFF2Loader(filename); + loader.loadFont(); + return loader.getFont(); + } + + private void loadFont() throws IOException { + String s; + s = openSection(); + if (!PFF2Sections.FILE.equals(s)) + throw new FileFormatException( + "First PFF2 section must be FILE but is `" + s + "'."); + + s = readCompleteSectionAsString(); + if (!"PFF2".equals(s)) + throw new FileFormatException("Unsupported file type `" + s + "'."); + + font.setFamily("Unnamed"); + font.setPointSize(Font.UNKNOWN_POINT_SIZE); + try { + if (debug >= 1) System.out.println("Loading font information..."); + boolean doneLoadingInfo = false; + while (!doneLoadingInfo) { + s = openSection(); + int sectionLength = f.readInt(); + if (debug >= 2) + System.out.println("Section: " + s + " length: " + sectionLength); + if (s.equals(PFF2Sections.MAX_CHAR_WIDTH)) { + int maxCharWidth = readSectionContentsAsUShort(sectionLength); + font.setMaxCharWidth(maxCharWidth); + } else if (s.equals(PFF2Sections.MAX_CHAR_HEIGHT)) { + int maxCharHeight = readSectionContentsAsUShort(sectionLength); + font.setMaxCharHeight(maxCharHeight); + } else if (s.equals(PFF2Sections.FONT_POINT_SIZE)) { + int pointSize = readSectionContentsAsUShort(sectionLength); + font.setPointSize(pointSize); + } else if (s.equals(PFF2Sections.FONT_ASCENT)) { + int ascent = readSectionContentsAsUShort(sectionLength); + font.setAscent(ascent); + } else if (s.equals(PFF2Sections.FONT_DESCENT)) { + int descent = readSectionContentsAsUShort(sectionLength); + font.setDescent(descent); + } else if (s.equals(PFF2Sections.FONT_NAME)) { + String fontName = readSectionContentsAsString(sectionLength); + if (debug >= 1) + System.out.println("Loaded font: " + fontName); + } else if (s.equals(PFF2Sections.FONT_FAMILY)) { + font.setFamily(readSectionContentsAsString(sectionLength)); + } else if (s.equals(PFF2Sections.FONT_WEIGHT)) { + String weightString = readSectionContentsAsString(sectionLength); + font.setBold(weightString.equalsIgnoreCase("bold")); + } else if (s.equals(PFF2Sections.FONT_SLANT)) { + String slantString = readSectionContentsAsString(sectionLength); + font.setItalic(slantString.equalsIgnoreCase("italic")); + } else if (s.equals(PFF2Sections.CHAR_INDEX)) { + loadCharsFromIndex(sectionLength); + } else if (s.equals(PFF2Sections.REMAINDER_IS_DATA)) { + doneLoadingInfo = true; + } else { + // If the section is not handled, then we just skip it. + if (f.skipBytes(sectionLength) != sectionLength) { + System.err.println("Warning: Could not skip " + + sectionLength + + " bytes for section `" + s + "'."); + } + } + } + } catch (EOFException eofException) { + // End of file; done reading font. + } + f.close(); + } + + private void loadCharsFromIndex(int sectionLength) throws IOException { + long indexSectionEnd = f.getFilePointer() + sectionLength; + try { + while (f.getFilePointer() < indexSectionEnd) { + int codePoint = f.readInt(); + int storageFlags = f.readUnsignedByte(); + int offset = f.readInt(); + + if ((storageFlags & 0x07) == 0) { + Glyph g = loadUncompressedGlyph(codePoint, offset); + font.putGlyph(codePoint, g); + } + } + } catch (EOFException eofException) { + // End of file; simple return. + } + } + + private Glyph loadUncompressedGlyph(int codePoint, int fileOffset) throws IOException { + if (debug >= 3) + System.out.printf("Loading uncompressed glyph U+%04X at %d%n", codePoint, fileOffset); + + long savedPos = f.getFilePointer(); + + f.seek(fileOffset); + int width = f.readUnsignedShort(); + int height = f.readUnsignedShort(); + int offsetX = f.readShort(); + int offsetY = f.readShort(); + int deviceWidth = f.readShort(); + if (debug >= 3) System.out.println(" Size=" + width + "x" + height + + " offset=(" + offsetX + ", " + offsetY + + ") dWidth=" + deviceWidth); + byte[] bitmap = new byte[(width * height + 7) / 8]; + f.readFully(bitmap); + + f.seek(savedPos); + return new Glyph(codePoint, width, height, + offsetX, offsetY, deviceWidth, bitmap); + } + + private int readSectionContentsAsUShort(int sectionLength) throws IOException { + if (sectionLength != 2) + throw new FileFormatException( + "For uint16 section, expected 2 bytes but length is " + + sectionLength + "."); + return f.readUnsignedShort(); + } + + private String openSection() throws IOException { + byte[] b = new byte[4]; + f.readFully(b); + return new String(b, Charset.forName("US-ASCII")); + } + + private String readCompleteSectionAsString() throws IOException { + int n = f.readInt(); + return readSectionContentsAsString(n); + } + + private String readSectionContentsAsString(int n) throws IOException { + StringBuilder buf = new StringBuilder(); + while (n-- != 0) { + char c = (char) (f.readByte() & 0xFF); + buf.append(c); + } + return buf.toString(); + } + + public Font getFont() { + return font; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,19 @@ +package org.gnu.grub.fonttool; + +/** + * Section name constants for the PFF2 file format. + */ +public class PFF2Sections { + static final String FILE = "FILE"; + static final String FONT_NAME = "NAME"; + static final String FONT_FAMILY = "FAMI"; + static final String FONT_WEIGHT = "WEIG"; + static final String FONT_SLANT = "SLAN"; + static final String FONT_POINT_SIZE = "PTSZ"; + static final String MAX_CHAR_WIDTH = "MAXW"; + static final String MAX_CHAR_HEIGHT = "MAXH"; + static final String FONT_ASCENT = "ASCE"; + static final String FONT_DESCENT = "DESC"; + static final String CHAR_INDEX = "CHIX"; + static final String REMAINDER_IS_DATA = "DATA"; +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,133 @@ +package org.gnu.grub.fonttool; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +// TODO Add DEFLATE compressed blocks of characters. +public class PFF2Writer { + private RandomAccessFile f; + private String currentSection; + private long currentSectionStart; + + public PFF2Writer(String filename) throws FileNotFoundException { + this.f = new RandomAccessFile(filename, "rw"); + this.currentSection = null; + this.currentSectionStart = -1; + } + + public void writeFont(Font font) throws IOException { + // Write file type ID header. + writeSection(PFF2Sections.FILE, "PFF2"); + + writeSection(PFF2Sections.FONT_NAME, font.getStandardName()); + writeSection(PFF2Sections.FONT_FAMILY, font.getFamily()); + writeSection(PFF2Sections.FONT_WEIGHT, font.isBold() ? "bold" : "normal"); + writeSection(PFF2Sections.FONT_SLANT, font.isItalic() ? "italic" : "normal"); + if (font.getPointSize() != Font.UNKNOWN_POINT_SIZE) + writeShortSection(PFF2Sections.FONT_POINT_SIZE, font.getPointSize()); + + // Construct character definitions. + CharDefs charDefs = new CharDefs(font); + charDefs.buildDefinitions(); + + // Write max character width and height metrics. + writeShortSection(PFF2Sections.MAX_CHAR_WIDTH, charDefs.getMaxCharWidth()); + writeShortSection(PFF2Sections.MAX_CHAR_HEIGHT, charDefs.getMaxCharHeight()); + writeShortSection(PFF2Sections.FONT_ASCENT, font.getAscent()); + writeShortSection(PFF2Sections.FONT_DESCENT, font.getDescent()); + + // Write character index with pointers to the character definitions. + beginSection(PFF2Sections.CHAR_INDEX); + + // Determine the size of the index, so we can properly refer to the + // character definition offset in the index. The actual number of + // bytes written is compared to the calculated value to ensure we + // are correct. + final int indexStart = (int) f.getFilePointer(); + final int calculatedIndexLength = + font.getGlyphs().size() * (4 + 1 + 4); + final int charDefStart = indexStart + calculatedIndexLength + 8; + + for (CharStorageInfo storageInfo : charDefs.getCharIndex().values()) { + f.writeInt(storageInfo.getCodePoint()); + f.writeByte(0); // Storage flags: bits 1..0 = 00b : uncompressed. + f.writeInt(charDefStart + storageInfo.getFileOffset()); + } + + final int indexEnd = (int) f.getFilePointer(); + if (indexEnd - indexStart != calculatedIndexLength) { + throw new RuntimeException("Incorrect index length calculated, calc=" + + calculatedIndexLength + + " actual=" + (indexEnd - indexStart)); + } + endSection(PFF2Sections.CHAR_INDEX); + + f.writeBytes(PFF2Sections.REMAINDER_IS_DATA); + f.writeInt(-1); // Data takes up the rest of the file. + f.write(charDefs.getDefinitionData()); + + f.close(); + } + + private void beginSection(String sectionName) throws IOException { + verifyOkToBeginSection(sectionName); + + f.writeBytes(sectionName); + f.writeInt(-1); // Placeholder for the section length. + currentSection = sectionName; + currentSectionStart = f.getFilePointer(); + } + + private void endSection(String sectionName) throws IOException { + verifyOkToEndSection(sectionName); + + long sectionEnd = f.getFilePointer(); + long sectionLength = sectionEnd - currentSectionStart; + f.seek(currentSectionStart - 4); + f.writeInt((int) sectionLength); + f.seek(sectionEnd); + currentSection = null; + currentSectionStart = -1; + } + + private void verifyOkToBeginSection(String sectionName) { + if (sectionName.length() != 4) + throw new IllegalArgumentException( + "Section names must be 4 characters: `" + sectionName + "'."); + if (currentSection != null) + throw new IllegalStateException( + "Attempt to start `" + sectionName + + "' section before ending the previous section `" + + currentSection + "'."); + } + + private void verifyOkToEndSection(String sectionName) { + if (sectionName.length() != 4) + throw new IllegalStateException("Invalid section name '" + sectionName + + "'; must be 4 characters."); + if (currentSection == null) + throw new IllegalStateException( + "Attempt to end section `" + sectionName + + "' when no section active."); + if (!sectionName.equals(currentSection)) + throw new IllegalStateException( + "Attempt to end `" + sectionName + + "' section during active section `" + + currentSection + "'."); + } + + private void writeSection(String sectionName, String contents) throws IOException { + verifyOkToBeginSection(sectionName); + f.writeBytes(sectionName); + f.writeInt(contents.length()); + f.writeBytes(contents); + } + + private void writeShortSection(String sectionName, int value) throws IOException { + verifyOkToBeginSection(sectionName); + f.writeBytes(sectionName); + f.writeInt(2); + f.writeShort(value); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Viewer.java' --- util/fonttool/src/org/gnu/grub/fonttool/Viewer.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Viewer.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,135 @@ +package org.gnu.grub.fonttool; + +import java.io.File; +import java.io.IOException; +import javax.swing.*; + +/** + * Program to view fonts. + *

+ * Supports PFF2 and BDF format font files. + */ +public class Viewer { + public static void main(String[] args) { + if (args.length < 1) { + printUsageAndExit(); + } + + boolean verbose = false; + String format = null; + String filename = null; + + try { + for (String arg : args) { + if (arg.startsWith("--")) { + String option; + String value; + int equalsPos = arg.indexOf('='); + if (equalsPos < 0) { + option = arg.substring(2); + value = null; + } else { + option = arg.substring(2, equalsPos); + value = arg.substring(equalsPos + 1); + } + + if ("format".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + format = value; + } else if ("verbose".equals(option)) { + verbose = true; + } + } else { + if (filename == null) { + filename = arg; + } else { + throw new CommandLineException( + "Extra argument after filename: `" + arg + "'."); + } + } + } + if (filename == null) { + throw new CommandLineException("A filename must be specified."); + } + if (!(format == null || "bdf".equals(format) || "pff2".equals(format))) { + throw new CommandLineException("Invalid format: must be `bdf' or `pff2'."); + } + } catch (CommandLineException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + + if (!new File(filename).exists()) { + System.err.println("Error: File `" + filename + "' not found."); + System.exit(1); + } + + if (format == null) { + // Autodetect the format. + if (PFF2Loader.isPFF2File(filename)) + format = "pff2"; + else if (BDFLoader.isBDFFile(filename)) + format = "bdf"; + else { + System.err.println("Error: Unable to detect font file format " + + "for `" + filename + "'."); + System.exit(1); + } + if (verbose) + System.out.println("Detected file format `" + format + "'."); + } + + Font font = null; + try { + if ("bdf".equals(format)) { + if (verbose) + System.out.println("Loading BDF font."); + font = BDFLoader.loadFontFile(filename); + } else if ("pff2".equals(format)) { + if (verbose) + System.out.println("Loading PFF2 font."); + font = PFF2Loader.loadFontFile(filename); + } else { + System.err.println("Error: Bad format `" + format + "'"); + System.exit(1); + } + } catch (IOException e) { + System.err.println("I/O error loading font: " + e); + e.printStackTrace(); + System.exit(1); + } + + if (font == null) { + System.err.println("Error: Font not loaded."); + System.exit(1); + } + + if (verbose) { + System.out.println("Loaded font: '" + font.getStandardName() + "'"); + } + + ViewerFrame f = new ViewerFrame(font); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.setLocationRelativeTo(null); // Center the frame on screen. + f.setVisible(true); + } + + private static class CommandLineException extends Exception { + public CommandLineException(String message) { + super(message); + } + } + + private static void printUsageAndExit() { + System.err.println("GNU GRUB Font Viewer"); + System.err.println(); + System.err.println("Usage: Viewer [--format=FORMAT] FILE"); + System.err.println(" --format=bdf The font is in BDF format."); + System.err.println(" --format=pff2 The font is in PFF2 format."); + System.err.println(" If no --format option is given, the type will be"); + System.err.println(" automatically detected."); + System.err.println(); + System.exit(1); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java' --- util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,61 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import javax.swing.*; +import javax.swing.border.BevelBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +public class ViewerFrame extends JFrame { + private final Font font; + + private JLabel startingCodePointLabel; + private JSlider startingCodePointSlider; + private CharMatrix charMatrix; + private CharView charView; + private JLabel statusBar; + + public ViewerFrame(Font font) { + this.font = font; + + createComponents(); + buildUI(); + setSize(800, 600); + } + + private void createComponents() { + startingCodePointLabel = new JLabel("Starting Code Point"); + startingCodePointSlider = new JSlider(JSlider.HORIZONTAL, 0, 65535, 0); + startingCodePointSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + charMatrix.setStartingChar(startingCodePointSlider.getValue()); + } + }); + charMatrix = new CharMatrix(); + charMatrix.setCharFont(font); + charMatrix.setGlyphSelectionListener(new CharMatrix.GlyphSelectionListener() { + public void glyphSelected(int codePoint) { + charView.setGlyph(font.getGlyph(codePoint)); + } + }); + charView = new CharView(); + charView.setZoom(3); + charView.setBorder(new BevelBorder(BevelBorder.RAISED)); + statusBar = new JLabel(); + } + + private void buildUI() { + JComponent topPanel = Box.createVerticalBox(); + topPanel.add(startingCodePointLabel); + topPanel.add(startingCodePointSlider); + + JComponent charPanel = new JPanel(new GridLayout(2, 1)); + charPanel.add(charMatrix); + charPanel.add(charView); + + setLayout(new BorderLayout()); + add(topPanel, BorderLayout.NORTH); + add(charPanel, BorderLayout.CENTER); + add(statusBar, BorderLayout.SOUTH); + } +} === added directory 'util/fonttool/test' === added directory 'util/fonttool/test/org' === added directory 'util/fonttool/test/org/gnu' === added directory 'util/fonttool/test/org/gnu/grub' === added directory 'util/fonttool/test/org/gnu/grub/fonttool' === added file 'util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java' --- util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,110 @@ +package org.gnu.grub.fonttool; + +import org.junit.Assert; +import org.junit.Test; + +public class TestGlyph { + @Test + public void testConstructor3x5() { + Glyph g = new Glyph(65, 3, 5, 0, 0, 0); + + // Should be empty upon construction. + assertBitmapEquals("..." + + "..." + + "..." + + "..." + + "...", + g.getBitmap()); + + // Should have ceil(3 * 5 / 8) = ceil(15 / 8) = ceil(1 + 7/8) = 2 bytes. + Assert.assertEquals("Wrong bitmap array size.", + 2, g.getBitmap().length); + } + + @Test + public void testConstructor5x5() { + Glyph g = new Glyph(65, 5, 5, 0, 0, 0); + + // Should be empty upon construction. + assertBitmapEquals("....." + + "....." + + "....." + + "....." + + ".....", + g.getBitmap()); + + // Should have ceil(5 * 5 / 8) = ceil(25 / 8) = ceil(3 + 1/8) = 4 bytes. + Assert.assertEquals("Wrong bitmap array size.", + 4, g.getBitmap().length); + } + + @Test + public void testSetPixel() { + Glyph g = new Glyph(65, 3, 5, 0, 0, 0); + + g.setPixel(0, 0, true); + Assert.assertEquals(0x80, g.getBitmap()[0] & 0xFF); + Assert.assertEquals(0x00, g.getBitmap()[1] & 0xFF); + assertBitmapEquals("X.." + + "..." + + "..." + + "..." + + "...", + g.getBitmap()); + + g.setPixel(2, 1, true); + Assert.assertEquals(0x84, g.getBitmap()[0] & 0xFF); + Assert.assertEquals(0x00, g.getBitmap()[1] & 0xFF); + assertBitmapEquals("X.." + + "..X" + + "..." + + "..." + + "...", + g.getBitmap()); + } + + static void assertBitmapEquals(String expectedBitmap, byte[] actualBitmap) { + Assert.assertNotNull("expected bitmap null", expectedBitmap); + Assert.assertNotNull("actual bitmap null", actualBitmap); + StringBuilder errors = new StringBuilder(); + + int bit = 7; + int byteIndex = -1; + for (int bitIndex = 0; bitIndex < expectedBitmap.length(); bitIndex++) { + char c = expectedBitmap.charAt(bitIndex); + boolean expectedBit; + if (c == '.') { + expectedBit = false; + } else if (c == 'X') { + expectedBit = true; + } else { + throw new IllegalArgumentException("Bad character '" + c + "' in expected bitmap string"); + } + + byteIndex = bitIndex / 8; + byte b = actualBitmap[byteIndex]; + boolean actualBit = (b & (1 << bit)) != 0; + if (actualBit != expectedBit) { + errors.append("[").append(bitIndex) + .append("] wrong (it is ") + .append(actualBit ? "1" : "0").append("). "); + } + + if (bit == 0) { + bit = 7; + } else { + bit--; + } + } + + if (byteIndex != expectedBitmap.length() / 8) { + errors.append("Last byte index wrong, was ") + .append(byteIndex).append(" but expected ") + .append(expectedBitmap.length() - 1); + } + + if (errors.length() != 0) { + Assert.fail("Bitmap wrong: " + errors); + } + } +} === modified file 'util/i386/pc/grub-mkrescue.in' --- util/i386/pc/grub-mkrescue.in 2008-04-18 15:42:57 +0000 +++ util/i386/pc/grub-mkrescue.in 2008-07-10 15:05:44 +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-10 14:59:42 +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,159 @@ 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. */ + + 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; + + 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; + 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 +888,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 +927,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 +998,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 +1095,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 +1129,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 +1141,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 +1177,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 +1220,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 +1256,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 +1292,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 +1335,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 +1450,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 +1705,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 +1810,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 +1852,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); === 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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