# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080719214404-ye3x3zjigausaru3 # target_branch: file:///home/cdb/grub/repo/trunk-clean/ # testament_sha1: f3f7e613f4952d31fe35294a437f872442769d50 # timestamp: 2008-07-19 14:49:37 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080719214342-aw2lim5mylg2crpc # # Begin patch === modified file '.bzrignore' --- .bzrignore 2008-07-19 21:43:42 +0000 +++ .bzrignore 2008-07-19 21:44:04 +0000 @@ -7,3 +7,5 @@ ./conf/sparc64-ieee1275.mk ./conf/i386-coreboot.mk ./build* +./util/fonttool/build +./util/fonttool/fonttool.iws === modified file 'ChangeLog' --- ChangeLog 2008-07-18 15:11:35 +0000 +++ ChangeLog 2008-07-19 21:39:45 +0000 @@ -332,6 +332,73 @@ * disk/ata.c: Use named constants for status bits. +2008-07-04 Colin D Bennett + + High resolution timer support. Implemented for i386 CPU using TSC. + Extracted generic grub_millisleep() so it's linked in only as needed. + This requires a Pentium compatible CPU; currently the code does not + check for this (so it will fail on 386 and 486 machines). + + * conf/i386-efi.rmk: Added TSC high resolution time module, link with + generic grub_millisleep() function. + + * conf/i386-pc.rmk: Likewise. + + * conf/sparc64-ieee1275.rmk: Add kern/generic/millisleep.c and + kern/generic/get_time_ms.c to kernel, to use generic time functions. + + * conf/powerpc-ieee1275.rmk: Add kern/generic/millisleep.c to kernel, + to use generic grub_millisleep() function. + + * conf/i386-linuxbios.rmk: Added kern/generic/get_time_ms.c to the + kernel. + + * kern/generic/get_time_ms.c (grub_get_time_ms): New file. Platform + independent implementation of grub_get_time_ms() using the RTC that + can be linked into a platform's kernel when it does not implement its + own specialized grub_get_time_ms() function. + + * kern/generic/millisleep.c (grub_millisleep): New file. Extracted + from grub_millisleep_generic() in kern/misc.c and renamed. Changed it + to use grub_get_time_ms() instead of grub_get_rtc() for better + precision on when high resolution time is available. + + * kern/misc.c (grub_millisleep_generic): Deleted. Moved to + kern/generic/millisleep.c so that it is only included in the kernel + image when a platform does not define a specialized version. + + * commands/sleep.c (grub_interruptible_millisleep): Uses + grub_get_time_ms() instead of grub_get_rtc() to stay in sync with + grub_millisleep() from kern/generic/millisleep.c. + + * include/grub/i386/tsc.h (grub_get_tsc): New file. Inline function + grub_get_tsc() uses x86 RDTSC instruction (available on Pentium+ CPUs) + to read the counter value for the TSC. + (grub_tsc_calibrate): Declare this function for grub_machine_init(). + + * kern/i386/tsc.c (grub_get_time_ms): x86 TSC support providing a high + resolution clock. + (grub_tsc_calibrate): New function to calibrate the TSC using RTC. + + * include/grub/time.h (grub_get_time_ms): Added grub_get_time_ms() + function to return the current time in millseconds since the epoch. + This supports higher resolution time than grub_get_rtc() on some + platforms such as i386-pc, where the RTC has only about 1/18 s + precision but a higher precision timer such as the TSC is available. + + * kern/i386/efi/init.c (grub_millisleep): Deleted. Don't define + grub_millisleep() -- it just called grub_millisleep_generic() but now + it is linked to kern/generic/millisleep.c for the implementation. + + * kern/sparc64/ieee1275/init.c (grub_millisleep): Deleted. + + * kern/i386/pc/init.c (grub_machine_init): Call grub_tsc_calibrate(). + (grub_millisleep): Deleted. + + * kern/ieee1275/init.c (grub_millisleep): Deleted. + (grub_get_rtc): Now calls grub_get_time_ms(), which does the real + work. + 2008-07-04 Pavel Roskin * kern/i386/linuxbios/init.c (grub_machine_init): Cast addr to === modified file 'Makefile.in' --- Makefile.in 2008-07-17 14:05:12 +0000 +++ Makefile.in 2008-07-19 21:39:24 +0000 @@ -85,6 +85,39 @@ YACC = @YACC@ UNIFONT_HEX = @UNIFONT_HEX@ +### Pretty output control ### +# Set up compiler and linker commands that either is quiet (does not print +# the command line being executed) or verbose (print the command line). +_CC := $(CC) +_TARGET_CC := $(TARGET_CC) +_STRIP := $(STRIP) +_GENMODSRC := sh $(srcdir)/genmodsrc.sh +ifeq ($(V),1) + override V_PREFIX := + override CC = $(_CC) + override TARGET_CC = $(_CC) + override STRIP = $(_STRIP) + override GENMODSRC = $(_GENMODSRC) + override INFO_GENCMDLIST = + override INFO_GENFSLIST = + override INFO_GENPARTMAPLIST = + override INFO_GEN_FINAL_COMMAND_LIST = + override INFO_GEN_FINAL_FS_LIST = + override INFO_GEN_FINAL_PARTMAP_LIST = +else + override V_PREFIX := @ + override CC = @echo "COMPILE $<"; $(_CC) + override TARGET_CC = @echo "COMPILE(TARGET) $<"; $(_TARGET_CC) + override STRIP = @echo "STRIP $@"; $(_STRIP) + override GENMODSRC = @echo "GENMODSRC $@"; $(_GENMODSRC) + override INFO_GENCMDLIST = @echo "GENCMDLIST $@" + override INFO_GENFSLIST = @echo "GENFSLIST $@" + override INFO_GENPARTMAPLIST = @echo "GENPARTMAPLIST $@" + override INFO_GEN_FINAL_COMMAND_LIST = @echo "GENCMDLIST[final] $@" + override INFO_GEN_FINAL_FS_LIST = @echo "GENFSLIST[final] $@" + override INFO_GEN_FINAL_PARTMAP_LIST = @echo "GENPARTMAPLIST[final] $@" +endif + # Options. enable_grub_emu = @enable_grub_emu@ enable_grub_fstest = @enable_grub_fstest@ @@ -132,13 +165,16 @@ || (rm -f $@; exit 1) command.lst: $(COMMANDFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_COMMAND_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ fs.lst: $(FSFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_FS_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ partmap.lst: $(PARTMAPFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_PARTMAP_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ ifeq (, $(UNIFONT_HEX)) else === modified file 'commands/i386/pc/vbeinfo.c' --- commands/i386/pc/vbeinfo.c 2007-07-21 22:32:33 +0000 +++ commands/i386/pc/vbeinfo.c 2008-07-09 14:39:39 +0000 @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -48,12 +49,22 @@ grub_err_t err; char *modevar; - grub_printf ("List of compatible video modes:\n"); - err = grub_vbe_probe (&controller_info); if (err != GRUB_ERR_NONE) return err; + int row = 0; + + grub_printf ("VBE info: version: %d.%d OEM software rev: %d.%d\n", + controller_info.version >> 8, + controller_info.version & 0xFF, + controller_info.oem_software_rev >> 8, + controller_info.oem_software_rev & 0xFF); + row++; + grub_printf (" total memory: %d KiB\n", + (controller_info.total_memory << 16) / 1024); + row++; + /* Because the information on video modes is stored in a temporary place, it is better to copy it to somewhere safe. */ p = video_mode_list = real2pm (controller_info.video_mode_ptr); @@ -67,6 +78,10 @@ grub_memcpy (saved_video_mode_list, video_mode_list, video_mode_list_size); + grub_printf ("List of compatible video modes:\n"); + grub_printf ("Legend: P=Packed pixel, D=Direct color, " + "mask/pos=R/G/B/reserved\n"); + /* Walk through all video modes listed. */ for (p = saved_video_mode_list; *p != 0xFFFF; p++) { @@ -103,10 +118,10 @@ switch (mode_info_tmp.memory_model) { case 0x04: - memory_model = "Packed Pixel"; + memory_model = "Packed"; break; case 0x06: - memory_model = "Direct Color"; + memory_model = "Direct"; break; default: @@ -116,12 +131,31 @@ if (! memory_model) continue; - grub_printf ("0x%03x: %d x %d x %d bpp (%s)\n", - mode, + grub_printf ("0x%03x: %4dx%4dx%2d %s", + mode, mode_info_tmp.x_resolution, mode_info_tmp.y_resolution, mode_info_tmp.bits_per_pixel, - memory_model); + memory_model); + if (memory_model[0] == 'D') + grub_printf (" mask: %d/%d/%d/%d pos: %d/%d/%d/%d", + mode_info_tmp.red_mask_size, + mode_info_tmp.green_mask_size, + mode_info_tmp.blue_mask_size, + mode_info_tmp.rsvd_mask_size, + mode_info_tmp.red_field_position, + mode_info_tmp.green_field_position, + mode_info_tmp.blue_field_position, + mode_info_tmp.rsvd_field_position); + grub_printf ("\n"); + + if (++row >= 25) + { + grub_printf ("-- More --"); + grub_getkey (); + grub_printf ("\r \r"); + row = 0; + } } grub_free (saved_video_mode_list); === modified file 'commands/sleep.c' --- commands/sleep.c 2008-05-16 20:55:29 +0000 +++ commands/sleep.c 2008-07-04 16:55:48 +0000 @@ -43,15 +43,15 @@ grub_printf ("%d ", n); } -/* Based on grub_millisleep() from kern/misc.c. */ +/* Based on grub_millisleep() from kern/generic/millisleep.c. */ static int grub_interruptible_millisleep (grub_uint32_t ms) { - grub_uint32_t end_at; - - end_at = grub_get_rtc () + grub_div_roundup (ms * GRUB_TICKS_PER_SECOND, 1000); - - while (grub_get_rtc () < end_at) + grub_uint64_t start; + + start = grub_get_time_ms (); + + while (grub_get_time_ms () - start < ms) if (grub_checkkey () >= 0 && GRUB_TERM_ASCII_CHAR (grub_getkey ()) == GRUB_TERM_ESC) return 1; === modified file 'commands/videotest.c' --- commands/videotest.c 2007-07-21 22:32:33 +0000 +++ commands/videotest.c 2008-07-09 16:14:04 +0000 @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -26,15 +27,51 @@ #include #include #include - -static grub_err_t -grub_cmd_videotest (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ +#include +#include +#include /* for grub_vbe_bios_set_display_start () test */ +/* Option array indices. */ +#define ARGINDEX_TEST_TIME 0 +#define ARGINDEX_DOUBLE_BUF 1 + +static const struct grub_arg_option arg_options[] = { + {"time", 't', 0, "Time to run each test, in seconds.", 0, ARG_TYPE_INT}, + {"dbuf", 'd', 0, "Use double buffered graphics.", 0, ARG_TYPE_NONE}, + {0, 0, 0, 0, 0, 0} +}; + +#define DEFAULT_TEST_TIME 5 + +/* Command options -- populated base on command line arguments. */ +struct videotest_options +{ + int test_time; + int double_buffering; +}; + +/* A 2D rectangle type. + * This could be worth integrating into the video API if it proves useful.*/ +struct grub_video_rect +{ + /* These are signed because if there are unsigned it causes Bad Things + * to happen when arithmetic and comparisions involving signed types is + * done. Important signed types include offsets from absolute locations. */ + int x; + int y; + int width; + int height; +}; +typedef struct grub_video_rect grub_video_rect_t; + +static void +basic_video_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; if (grub_video_setup (1024, 768, GRUB_VIDEO_MODE_TYPE_INDEX_COLOR) != GRUB_ERR_NONE) - return grub_errno; + return; grub_getkey (); @@ -44,7 +81,8 @@ unsigned int width; unsigned int height; int i; - struct grub_font_glyph glyph; + grub_font_t font; + struct grub_font_glyph *glyph; struct grub_video_render_target *text_layer; grub_video_color_t palette[16]; @@ -65,8 +103,15 @@ color = grub_video_map_rgb (0, 255, 255); grub_video_fill_rect (color, 100, 100, 100, 100); - grub_font_get_glyph ('*', &glyph); - grub_video_blit_glyph (&glyph, color, 200 ,0); + font = grub_font_get ("Helvetica Bold 14"); + if (! font) + { + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + glyph = grub_font_get_glyph (font, '*'); + grub_video_blit_glyph (glyph, color, 200 ,0); grub_video_set_viewport (x + 150, y + 150, width - 150 * 2, height - 150 * 2); @@ -77,18 +122,18 @@ color = grub_video_map_rgb (255, 255, 255); - grub_font_get_glyph ('A', &glyph); - grub_video_blit_glyph (&glyph, color, 16, 16); - grub_font_get_glyph ('B', &glyph); - grub_video_blit_glyph (&glyph, color, 16 * 2, 16); + glyph = grub_font_get_glyph (font, 'A'); + grub_video_blit_glyph (glyph, color, 16, 16); + glyph = grub_font_get_glyph (font, 'B'); + grub_video_blit_glyph (glyph, color, 16 * 2, 16); - grub_font_get_glyph ('*', &glyph); + glyph = grub_font_get_glyph (font, '*'); for (i = 0; i < 16; i++) { color = grub_video_map_color (i); palette[i] = color; - grub_video_blit_glyph (&glyph, color, 16 + i * 16, 32); + grub_video_blit_glyph (glyph, color, 16 + i * 16, 32); } grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); @@ -101,30 +146,1076 @@ 0, 0, width, height); } + grub_video_swap_buffers (); + grub_getkey (); + + /* Test VBE set display start address. */ + /* This should scroll the screen, first vertically, + * then horizontally. The horizontal scrolling seems to + * only have a resolution of about 16 pixels on my VIA Mini-ITX. */ + int vbestatus; + int vbeok = 0; + int vbeerr = 0; + for (i = 0; i < 50; i++) + { + vbestatus = grub_vbe_bios_set_display_start (0, i); + if (vbestatus == GRUB_VBE_STATUS_OK) + vbeok++; + else + vbeerr++; + } + + + grub_getkey (); + + for (i = 0; i < 50; i++) + { + vbestatus = grub_vbe_bios_set_display_start (i, 50); + if (vbestatus == GRUB_VBE_STATUS_OK) + vbeok++; + else + vbeerr++; + } + grub_getkey (); grub_video_delete_render_target (text_layer); - - grub_video_restore (); - - for (i = 0; i < 16; i++) - grub_printf("color %d: %08x\n", i, palette[i]); - - grub_errno = GRUB_ERR_NONE; + grub_video_restore (); + + grub_printf ("VBE set_display_start status: %d\n", vbestatus); + grub_printf ("ok: %d\n", vbeok); + grub_printf ("errors: %d\n", vbeerr); + + grub_errno = GRUB_ERR_NONE; +} + + + +/** + * Simple opaque image blit test. + * Returns the error status in grub_errno. + */ +static void +bitmap_demo (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + const int bm1width = 500, bm1height = 400; + struct grub_video_bitmap *bitmap1; + + if (grub_video_bitmap_create (&bitmap1, bm1width, bm1height, + GRUB_VIDEO_BLIT_FORMAT_RGB_888) + != GRUB_ERR_NONE) + return; + + int offset = 0; + int x; + int y; + grub_uint8_t *data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bm1height; y++) + { + for (x = 0; x < bm1width; x++) + { + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + } + } + + /* Blit the entire bitmap in the center of the screen. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + (view.width - bm1width) / 2, + view.y + (view.height - bm1height) / 2, + 0, 0, bm1width, bm1height); + grub_video_swap_buffers (); + grub_getkey (); + + /* Blit more copies of the bitmap. */ + /* Upper left. */ + grub_video_blit_bitmap (bitmap1, GRUB_VIDEO_BLIT_REPLACE, + view.x, view.y, 0, 0, bm1width, bm1height); + /* Upper right. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + view.width - bm1width, + view.y, 0, 0, bm1width, bm1height); + /* Lower left. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x, + view.y + view.height - bm1height, + 0, 0, bm1width, bm1height); + /* Lower right. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + view.width - bm1width, + view.y + view.height - bm1height, + 0, 0, bm1width, bm1height); + grub_video_swap_buffers (); + grub_getkey (); + + int clearbg = 0; /* Boolean flag: whether to fill background. */ + grub_video_color_t bgcolor = grub_video_map_rgb (16, 16, 96); + + /* Animate the image sliding in. End when a key is pressed. */ + int vscale = 1000; + int velocityx = -5000; + int velocityy = -8000; + int positionx = 100; + int positiony = 300; + + grub_uint32_t frame_count = 0; + grub_uint32_t start_time = grub_get_time_ms (); + + while (grub_checkkey () == -1) + { + /* If the time limit option is set, then check if it's exceeded. */ + if (vt_opts->test_time != 0 + && grub_get_time_ms () >= start_time + vt_opts->test_time * 1000) + break; + + int newx = positionx + velocityx / vscale; + int newy = positiony + velocityy / vscale; + + /* Check collision w/ left */ + if (newx < view.x && velocityx < 0) + { + velocityx = -velocityx; + newx = positionx + velocityx / vscale; + } + /* Check collision w/ right */ + if (newx + bm1width > view.x + view.width && velocityx > 0) + { + velocityx = -velocityx; + newx = positionx + velocityx / vscale; + } + /* Check collision w/ top */ + if (newy < 0 && velocityy < 0) + { + velocityy = -velocityy; + newy = positiony + velocityy / vscale; + } + /* Check collision w/ bottom */ + if (newy + bm1height > view.y + view.height && velocityy > 0) + { + velocityy = -velocityy; + newy = positiony + velocityy / vscale; + } + + positionx = newx; + positiony = newy; + + if (clearbg) + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, + view.height); + + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + positionx, + view.y + positiony, 0, 0, bm1width, bm1height); + grub_video_swap_buffers (); + frame_count++; + + /* Acceleration due to gravity... + * note that the y coordinate is increasing downward. */ + velocityy += vscale / 7; + } + + /* Calculate average frame rate. */ + grub_uint32_t duration = grub_get_time_ms () - start_time; + grub_uint32_t fps_x10 = 10 * 1000 * frame_count / duration; + + /* Eat the keystroke. */ + if (grub_checkkey () != -1) + grub_getkey (); + + grub_video_bitmap_destroy (bitmap1); + grub_video_restore (); + + grub_printf ("Average frame rate: %d.%d fps\n", fps_x10 / 10, fps_x10 % 10); + + grub_errno = GRUB_ERR_NONE; +} + + + +/* Configuration settings for a benchmark run. */ +struct benchmark_config +{ + int width; + int height; + unsigned int mode_type; + int use_rgba_bitmaps; +}; + +/* Macro to make the benchmark_configs[] declaration more concise. */ +#define MODE_TYPE_RGB(bpp) (GRUB_VIDEO_MODE_TYPE_RGB \ + | ((bpp) << GRUB_VIDEO_MODE_TYPE_DEPTH_POS)) + +/* The video configurations to use for the benchmark. */ +static struct benchmark_config benchmark_configs[] = { + {320, 200, MODE_TYPE_RGB (0), 0}, + {640, 480, MODE_TYPE_RGB (0), 0}, + {1024, 768, MODE_TYPE_RGB (0), 0}, + {1024, 768, MODE_TYPE_RGB (0), 1}, +}; + +#define NUM_BENCHMARK_CONFIGS (sizeof(benchmark_configs) \ + / sizeof(benchmark_configs[0])) + +struct benchmark_result +{ + /* If set to 1, the test was able to run successfully; + * 0 means there was an error. */ + int test_passed; + + /* Bits per pixel for the video mode used. */ + int bpp; + + /* All fps are in fps * 10 to achieve one decimal place. */ + /* Set to 0 to indicate the test could not be run. */ + grub_int32_t fill_fps; + grub_int32_t blit_fps; + grub_int32_t blend_fps; +}; + +#define BENCHMARK_RESULT_FPS_SCALE 10 + +static void +move_rectangle_one_step (int *x, int *y, + int width, int height, + int *vx, int *vy, const grub_video_rect_t * bounds) +{ + int newx = *x + *vx; + int newy = *y + *vy; + + /* Check collision w/ left */ + if (newx < bounds->x && *vx < 0) + { + *vx = -*vx; + newx = *x + *vx; + } + /* Check collision w/ right */ + if (newx + width > bounds->x + bounds->width && *vx > 0) + { + *vx = -*vx; + newx = *x + *vx; + } + /* Check collision w/ top */ + if (newy < 0 && *vy < 0) + { + *vy = -*vy; + newy = *y + *vy; + } + /* Check collision w/ bottom */ + if (newy + height > bounds->y + bounds->height && *vy > 0) + { + *vy = -*vy; + newy = *y + *vy; + } + + *x = newx; + *y = newy; +} + +/** + * Run the benchmark test for a particular video mode, which is specified + * by ``*config``. The results of the test are stored in ``*result``. + */ +static void +do_benchmark (const struct benchmark_config *config, + struct benchmark_result *result, + struct videotest_options *vt_opts) +{ + struct grub_video_mode_info modeinfo; + + result->test_passed = 0; + result->fill_fps = 0; + result->blit_fps = 0; + result->blend_fps = 0; + result->bpp = 0; + + int mode_type = config->mode_type; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (config->width, config->height, + mode_type) != GRUB_ERR_NONE) + return; + + if (grub_video_get_info (&modeinfo) == GRUB_ERR_NONE) + result->bpp = modeinfo.bpp; + + /* Full screen bitmap blit test. */ + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + + /* For measuring timing. */ + grub_uint32_t frame_count; + grub_uint64_t start_time; + grub_uint64_t desired_stop_time; + grub_uint32_t duration; + + + /*** FILL TEST ***/ + + /* Alternates between 0 and 1 to change the color. */ + int color_flag = 0; + grub_video_color_t fillcolors[2]; + fillcolors[0] = grub_video_map_rgb (0, 0, 255); + fillcolors[1] = grub_video_map_rgb (255, 255, 0); + + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + grub_video_fill_rect (fillcolors[color_flag], + view.x, view.y, view.width, view.height); + grub_video_swap_buffers (); + color_flag ^= 1; + frame_count++; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was one. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->fill_fps = 0; + } + else + { + result->fill_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + + /*** BLIT TEST ***/ + + /* Generate two bitmaps, the same size as the screen. */ + const int bitmapwidth = view.width, bitmapheight = view.height; + struct grub_video_bitmap *bitmap1; + struct grub_video_bitmap *bitmap2; + enum grub_video_blit_format bitmap_format = config->use_rgba_bitmaps + ? GRUB_VIDEO_BLIT_FORMAT_RGBA_8888 : GRUB_VIDEO_BLIT_FORMAT_RGB_888; + + if (grub_video_bitmap_create (&bitmap1, bitmapwidth, bitmapheight, + bitmap_format) != GRUB_ERR_NONE) + return; + + if (grub_video_bitmap_create (&bitmap2, bitmapwidth, bitmapheight, + bitmap_format) != GRUB_ERR_NONE) + { + grub_video_bitmap_destroy (bitmap1); + return; + } + + int offset; + int x; + int y; + grub_uint8_t *data; + + offset = 0; + data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bitmapheight; y++) + { + for (x = 0; x < bitmapwidth; x++) + { + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + if (config->use_rgba_bitmaps) + data[offset++] = 255; + } + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap2); + for (y = 0; y < bitmapheight; y++) + { + for (x = 0; x < bitmapwidth; x++) + { + data[offset++] = x + y; /* red */ + data[offset++] = x * x + y * y; /* green */ + data[offset++] = x * x / 4 + y * y / 4; /* blue */ + if (config->use_rgba_bitmaps) + data[offset++] = 255; + } + } + + + /* Now do the blit test, alternating between the two bitmaps. */ + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + int cur = 0; /* Which bitmap to draw this frame. */ + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + struct grub_video_bitmap *current_bitmap = cur == 0 ? bitmap1 : bitmap2; + grub_video_blit_bitmap (current_bitmap, + GRUB_VIDEO_BLIT_REPLACE, + view.x, view.y, 0, 0, bitmapwidth, + bitmapheight); + grub_video_swap_buffers (); + frame_count++; + cur ^= 1; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was once. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->blit_fps = 0; + } + else + { + result->blit_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + grub_video_bitmap_destroy (bitmap2); + grub_video_bitmap_destroy (bitmap1); + + + + /*** BLEND TEST ***/ + + /* Generate two bitmaps, with alpha translucency. */ + const int bbw = view.width * 2 / 3; + const int bbh = view.height * 2 / 3; + + if (grub_video_bitmap_create (&bitmap1, bbw, bbh, + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) != + GRUB_ERR_NONE) + return; + + if (grub_video_bitmap_create (&bitmap2, bbw, bbh, + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) != + GRUB_ERR_NONE) + { + grub_video_bitmap_destroy (bitmap1); + return; + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bbh; y++) + { + for (x = 0; x < bbw; x++) + { + /* Calculate a to be increasing away from the center. */ + int dx = 256 * (x - bbw / 2) / (bbw / 2); + int dy = 256 * (y - bbh / 2) / (bbh / 2); + int a = dx * dx + dy * dy; + /* range for a = 0 .. 2*(256^2) = 2*2^16 = 2^17 */ + a >>= 17 - 8; /* Make range 0..256. */ + if (a > 255) + a = 255; + + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + data[offset++] = 255 - a; + } + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap2); + for (y = 0; y < bbh; y++) + { + for (x = 0; x < bbw; x++) + { + data[offset++] = x + y; /* red */ + data[offset++] = x * x + y * y; /* green */ + data[offset++] = x * x / 4 + y * y / 4; /* blue */ + data[offset++] = 255; + } + } + + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + + grub_video_color_t bgcolor = grub_video_map_rgb (80, 80, 80); + + /* Bitmap locations. */ + int b1x = 0; + int b1y = 0; + int b2x = view.width - bbw; + int b2y = 0; + /* Bitmap velocities. */ + int b1vx = 8; + int b1vy = 12; + int b2vx = -10; + int b2vy = 9; + + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + move_rectangle_one_step (&b1x, &b1y, bbw, bbh, &b1vx, &b1vy, &view); + move_rectangle_one_step (&b2x, &b2y, bbw, bbh, &b2vx, &b2vy, &view); + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height); + grub_video_blit_bitmap (bitmap2, + GRUB_VIDEO_BLIT_BLEND, + b2x, b2y, 0, 0, bbw, bbh); + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_BLEND, + b1x, b1y, 0, 0, bbw, bbh); + grub_video_swap_buffers (); + frame_count++; + cur ^= 1; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was once. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->blend_fps = 0; + } + else + { + result->blend_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + grub_video_bitmap_destroy (bitmap2); + grub_video_bitmap_destroy (bitmap1); + + grub_video_restore (); + result->test_passed = 1; +} + +/** + * Run a benchmark test in a series of video modes. + * The results are reported in tabular form. This will be helpful to + * determine how effective various optimizations are. + */ +static void +benchmark_test (struct videotest_options *vt_opts) +{ + unsigned int i; + struct benchmark_result results[NUM_BENCHMARK_CONFIGS]; + + /* Set option default values. */ + if (vt_opts->test_time == 0) + vt_opts->test_time = DEFAULT_TEST_TIME; + + /* Run benchmarks. */ + for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++) + { + grub_error_push (); + do_benchmark (&benchmark_configs[i], &results[i], vt_opts); + } + + grub_print_error (); + + /* Display results. */ + grub_printf ("Benchmark results (in frames/s):\n"); + grub_printf ("(W=Width, H=Height, B=Bits per pixel,\n" + " T=Mode Type, A=Bitmap Alpha)\n"); + grub_printf (" W H B T A FILL BLIT BLEND\n"); + for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++) + { + struct benchmark_config *c = &benchmark_configs[i]; + struct benchmark_result *r = &results[i]; + + if (r->test_passed) + { + grub_printf ("%4dx%4d %2d %d %d: %4d.%d %4d.%d %4d.%d\n", + c->width, c->height, r->bpp, + c->mode_type & 0x0F, + c->use_rgba_bitmaps, + r->fill_fps / BENCHMARK_RESULT_FPS_SCALE, + r->fill_fps % BENCHMARK_RESULT_FPS_SCALE, + r->blit_fps / BENCHMARK_RESULT_FPS_SCALE, + r->blit_fps % BENCHMARK_RESULT_FPS_SCALE, + r->blend_fps / BENCHMARK_RESULT_FPS_SCALE, + r->blend_fps % BENCHMARK_RESULT_FPS_SCALE); + } + else + { + grub_printf ("%4dx%4d %2d %d Not supported.\n", + c->width, c->height, + ((c->mode_type & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK) + >> GRUB_VIDEO_MODE_TYPE_DEPTH_POS), + c->mode_type & 0x0F); + } + } + + grub_errno = GRUB_ERR_NONE; +} + + +/** + * Test time functions. + */ +static void +clock_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + /* Draw a progress bar that animates in sync with time. */ + + grub_video_rect_t bar_frame; + bar_frame.width = view.width - 2 * view.width / 10; + bar_frame.height = view.height / 20; + bar_frame.x = view.x + view.width / 10; + bar_frame.y = view.y + view.height - bar_frame.height - view.height / 10; + + grub_video_color_t bgcolor = grub_video_map_rgb (50, 50, 50); + grub_video_color_t framecolor = grub_video_map_rgb (255, 255, 255); + grub_video_color_t barbgcolor = grub_video_map_rgb (0, 0, 128); + grub_video_color_t barcolor = grub_video_map_rgb (100, 100, 255); + + grub_uint32_t frame_count; + grub_uint64_t start_time; + grub_uint64_t barstart; + grub_uint32_t barlength = 1000; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + frame_count = 0; + start_time = grub_get_time_ms (); + barstart = grub_get_time_ms (); + + while (grub_checkkey () == -1) + { + grub_uint64_t now; + grub_uint32_t bartime; + now = grub_get_time_ms (); + /* If the time limit option is set, then check if it's exceeded. */ + if (vt_opts->test_time != 0 + && now >= start_time + vt_opts->test_time * 1000) + break; + bartime = now - barstart; + if (bartime > barlength) + { + barstart = grub_get_time_ms (); /* Start over. */ + bartime = barlength; + } + + /* Clear screen. */ + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height); + + /* Border. */ + grub_video_fill_rect (framecolor, + bar_frame.x - 1, bar_frame.y - 1, + bar_frame.width + 2, bar_frame.height + 2); + + /* Bar background. */ + int barwidth = bar_frame.width * bartime / barlength; + grub_video_fill_rect (barbgcolor, bar_frame.x + barwidth, + bar_frame.y, bar_frame.width - barwidth, + bar_frame.height); + /* Bar foreground. */ + grub_video_fill_rect (barcolor, bar_frame.x, bar_frame.y, + barwidth, bar_frame.height); + grub_video_swap_buffers (); + frame_count++; + } + + grub_uint32_t duration = grub_get_time_ms () - start_time; + grub_uint32_t fps_x10 = 10 * 1000 * frame_count / duration; + + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was one. */ + grub_video_restore (); + grub_printf ("Average frame rate: %d.%d fps\n", fps_x10 / 10, fps_x10 % 10); +} + + +/** + * Test double buffering. + */ +static void +doublebuf_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + grub_video_color_t red = grub_video_map_rgb (255, 50, 50); + grub_video_color_t yellow = grub_video_map_rgb (255, 255, 0); + grub_video_color_t green = grub_video_map_rgb (20, 255, 20); + grub_video_color_t blue = grub_video_map_rgb (50, 50, 255); + grub_video_color_t black = grub_video_map_rgb (0, 0, 0); + grub_video_color_t bgcolor = grub_video_map_rgb (255, 255, 255); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (red, 100, 100, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (yellow, 120, 120, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (green, 140, 140, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (blue, 160, 160, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (black, 180, 180, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_restore (); +} + + +/** + * Test text rendering. + */ +static void +text_test (struct videotest_options *vt_opts) +{ + grub_video_color_t color; + const char *s; + int view_x; + int view_y; + int view_width; + int view_height; + int xpos; + int ypos; + int i; + grub_font_t font1; + grub_font_t font2; + grub_font_t font3; + + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_get_viewport ((unsigned int *) &view_x, + (unsigned int *) &view_y, + (unsigned int *) &view_width, + (unsigned int *) &view_height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + if (!(font1 = grub_font_get ("New Century Schoolbook 24")) + || !(font2 = grub_font_get ("Helvetica Bold 14")) + || !(font3 = grub_font_get ("Helvetica 10"))) + { + grub_video_restore (); + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + color = grub_video_map_rgb (0, 0, 0); + grub_video_fill_rect (color, 0, 0, view_width, view_height); + + color = grub_video_map_rgb (255, 0, 0); + grub_video_fill_rect (color, 0, 0, 100, 100); + + color = grub_video_map_rgb (0, 255, 255); + grub_video_fill_rect (color, 100, 100, 100, 100); + + color = grub_video_map_rgb (255, 255, 255); + + xpos = 10; + ypos = 30; + s = "Hello, World!"; + for (i = 0; i < 40; i++) + { + if (xpos + grub_font_get_string_width (font1, s) >= view_width) + { + /* The string will wrap; go to the beginning of the next line. */ + xpos = 10; + ypos += (grub_font_get_descent (font1) + + grub_font_get_ascent (font1)); + } + grub_video_draw_string ("Hello, World!", + font1, grub_video_map_rgb (255, 255, 0), + view_x + xpos, view_y + ypos); + + xpos += grub_font_get_string_width (font1, s); + } + + xpos = 300; + ypos = 450; + grub_video_draw_string (grub_font_get_name (font1), + font1, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + ypos += grub_font_get_descent (font1) + grub_font_get_ascent (font2) + 2; + grub_video_draw_string (grub_font_get_name (font2), + font2, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + ypos += grub_font_get_descent (font2) + grub_font_get_ascent (font3) + 2; + grub_video_draw_string (grub_font_get_name (font3), + font3, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + + grub_video_swap_buffers (); + grub_getkey (); + grub_video_restore (); + grub_errno = GRUB_ERR_NONE; +} + + +/** + * Test bitmap scaling. + */ +static void +scale_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_errno = GRUB_ERR_NONE; + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + grub_font_t font; + if (!(font = grub_font_get ("Helvetica 10"))) + { + grub_video_restore (); + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + grub_video_color_t color; + + int text_y = 0; + int text_height = 25; + + color = grub_video_map_rgb (44, 44, 200); + grub_video_fill_rect (color, view.x, view.y, view.width, view.height); + color = grub_video_map_rgb (255, 255, 255); + + grub_video_draw_string ("Loading image", + font, color, 10, text_y += text_height); + + enum grub_video_bitmap_scale_method scale_method = + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST; + const char *bitmap_name = "/boot/images/wallpaper.tga"; + struct grub_video_bitmap *bitmap; + grub_video_bitmap_load (&bitmap, bitmap_name); + if (grub_errno != GRUB_ERR_NONE) + { + grub_video_draw_string ("Error loading bitmap", + font, color, 10, text_y += text_height); + } + else + { + grub_video_draw_string ("Original image", + font, color, 10, text_y += text_height); + grub_video_blit_bitmap (bitmap, GRUB_VIDEO_BLIT_BLEND, + 400, text_y - 10, + 0, 0, grub_video_bitmap_get_width (bitmap), + grub_video_bitmap_get_height (bitmap)); + + struct grub_video_bitmap *bitmap2; + if (grub_video_bitmap_create_scaled (&bitmap2, 40, 40, + bitmap, + scale_method) + != GRUB_ERR_NONE) + { + grub_video_draw_string ("Error scaling down bitmap", + font, color, 10, text_y += text_height); + } + else + { + grub_video_draw_string ("Scaled down version", + font, color, 10, text_y += text_height); + grub_video_blit_bitmap (bitmap2, GRUB_VIDEO_BLIT_BLEND, + 400, text_y + 100, + 0, 0, grub_video_bitmap_get_width (bitmap2), + grub_video_bitmap_get_height (bitmap2)); + grub_video_bitmap_destroy (bitmap2); + } + + struct grub_video_bitmap *bitmap3; + if (grub_video_bitmap_create_scaled (&bitmap3, 500, 300, bitmap, + scale_method) + != GRUB_ERR_NONE) + { + grub_video_draw_string ("Error scaling up bitmap", + font, color, 10, text_y += text_height); + } + else + { + grub_video_draw_string ("Scaled up version", + font, color, 10, text_y += text_height); + grub_video_blit_bitmap (bitmap3, GRUB_VIDEO_BLIT_BLEND, + 400, text_y + 50, + 0, 0, grub_video_bitmap_get_width (bitmap3), + grub_video_bitmap_get_height (bitmap3)); + grub_video_bitmap_destroy (bitmap3); + } + } + + grub_video_swap_buffers (); + grub_getkey (); + grub_video_bitmap_destroy (bitmap); + grub_video_restore (); +} + + +/** Print a list of the available tests. */ +static void list_tests (void); + +/** + * Video test command. Takes an argument specifying the test to run. + */ +static grub_err_t +grub_cmd_videotest (struct grub_arg_list *state, int argc, char **args) +{ + int i; + struct videotest_options vt_opts; + /* Pointer to the test function. */ + void (*test_func) (struct videotest_options *) = NULL; + + vt_opts.test_time = + state[ARGINDEX_TEST_TIME].set + ? grub_strtoul (state[ARGINDEX_TEST_TIME].arg, 0, 0) : 0; + vt_opts.double_buffering = state[ARGINDEX_DOUBLE_BUF].set; + + /* Parse command line arguments to determine the test to run. */ + for (i = 0; i < argc; i++) + { + char *arg = args[i]; + if (grub_strcmp (arg, "list") == 0) + { + list_tests (); + return GRUB_ERR_NONE; + } + else if (grub_strcmp (arg, "basic") == 0) + { + test_func = basic_video_test; + } + else if (grub_strcmp (arg, "bench") == 0) + { + test_func = benchmark_test; + } + else if (grub_strcmp (arg, "bitmaps") == 0) + { + test_func = bitmap_demo; + } + else if (grub_strcmp (arg, "clock") == 0) + { + test_func = clock_test; + } + else if (grub_strcmp (arg, "doublebuf") == 0) + { + test_func = doublebuf_test; + } + else if (grub_strcmp (arg, "text") == 0) + { + test_func = text_test; + } + else if (grub_strcmp (arg, "scale") == 0) + { + test_func = scale_test; + } + else + { + grub_printf ("Error: Unknown test `%s'\n", arg); + grub_errno = GRUB_ERR_BAD_ARGUMENT; + return grub_errno; + } + } + + if (test_func == NULL) + { + grub_printf ("Usage: videotest TESTNAME Run a test.\n"); + grub_printf (" videotest list List available tests.\n"); + grub_printf ("\n"); + list_tests (); + } + else + { + test_func (&vt_opts); + } + return grub_errno; } -GRUB_MOD_INIT(videotest) +static void +list_tests (void) +{ + grub_printf ("Available tests\n"); + grub_printf ("===============\n"); + grub_printf ("basic Basic video test with filled rectangles,\n"); + grub_printf (" offscreen rendering targets, some text.\n"); + grub_printf ("bench Run a performance benchmark.\n"); + grub_printf ("bitmaps Test generating and blitting bitmaps.\n"); + grub_printf ("clock Test time functions w/ animated progress bar.\n"); + grub_printf ("doublebuf Test double buffering.\n"); + grub_printf ("text Test text rendering.\n"); + grub_printf ("scale Test image scaling.\n"); + grub_printf ("\n"); +} + + +GRUB_MOD_INIT (videotest) { grub_register_command ("videotest", grub_cmd_videotest, GRUB_COMMAND_FLAG_BOTH, - "videotest", - "Test video subsystem", - 0); + "videotest TEST", + "Run the specified video subsystem test.", + arg_options); } -GRUB_MOD_FINI(videotest) +GRUB_MOD_FINI (videotest) { grub_unregister_command ("videotest"); } === modified file 'conf/common.rmk' --- conf/common.rmk 2008-07-11 19:09:14 +0000 +++ conf/common.rmk 2008-07-19 21:39:24 +0000 @@ -312,7 +312,7 @@ help_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'conf/i386-efi.rmk' --- conf/i386-efi.rmk 2008-07-17 08:50:26 +0000 +++ conf/i386-efi.rmk 2008-07-19 21:39:45 +0000 @@ -84,7 +84,9 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/efi/init.c kern/parser.c kern/partition.c \ kern/env.c symlist.c kern/efi/efi.c kern/efi/init.c kern/efi/mm.c \ - term/efi/console.c disk/efi/efidisk.c + term/efi/console.c disk/efi/efidisk.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c kernel_mod_HEADERS = arg.h boot.h cache.h device.h disk.h dl.h elf.h elfload.h \ env.h err.h file.h fs.h kernel.h loader.h misc.h mm.h net.h parser.h \ partition.h pc_partition.h rescue.h symbol.h term.h time.h types.h \ === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-07-13 00:55:15 +0000 +++ conf/i386-pc.rmk 2008-07-19 21:39:45 +0000 @@ -43,6 +43,8 @@ kern/disk.c kern/dl.c kern/file.c kern/fs.c kern/err.c \ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/pc/init.c kern/parser.c kern/partition.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c \ kern/env.c \ term/i386/pc/console.c \ symlist.c @@ -261,7 +263,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 */ === modified file 'include/grub/time.h' --- include/grub/time.h 2007-10-22 19:02:16 +0000 +++ include/grub/time.h 2008-07-03 04:19:16 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2007 Free Software Foundation, Inc. + * Copyright (C) 2007, 2008 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,12 +19,13 @@ #ifndef KERNEL_TIME_HEADER #define KERNEL_TIME_HEADER 1 +#include #include #include #include void EXPORT_FUNC(grub_millisleep) (grub_uint32_t ms); -void EXPORT_FUNC(grub_millisleep_generic) (grub_uint32_t ms); +grub_uint64_t EXPORT_FUNC(grub_get_time_ms) (void); static __inline void grub_sleep (grub_uint32_t s) === modified file 'include/grub/video.h' --- include/grub/video.h 2008-01-01 12:02:07 +0000 +++ include/grub/video.h 2008-07-03 14:12:08 +0000 @@ -21,6 +21,7 @@ #include #include +#include /* Video color in hardware dependent format. Users should not assume any specific coding format. */ @@ -31,40 +32,41 @@ struct grub_video_render_target; /* Forward declarations for used data structures. */ -struct grub_font_glyph; struct grub_video_bitmap; /* Defines used to describe video mode or rendering target. */ -#define GRUB_VIDEO_MODE_TYPE_ALPHA 0x00000008 -#define GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED 0x00000004 +#define GRUB_VIDEO_MODE_TYPE_ALPHA 0x00000020 +#define GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED 0x00000010 +#define GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP 0x00000004 #define GRUB_VIDEO_MODE_TYPE_INDEX_COLOR 0x00000002 #define GRUB_VIDEO_MODE_TYPE_RGB 0x00000001 /* Defines used to mask flags. */ -#define GRUB_VIDEO_MODE_TYPE_COLOR_MASK 0x00000003 +#define GRUB_VIDEO_MODE_TYPE_COLOR_MASK 0x0000000F /* Defines used to specify requested bit depth. */ #define GRUB_VIDEO_MODE_TYPE_DEPTH_MASK 0x0000ff00 #define GRUB_VIDEO_MODE_TYPE_DEPTH_POS 8 -/* Defined predefined render targets. */ -#define GRUB_VIDEO_RENDER_TARGET_DISPLAY ((struct grub_video_render_target *) 0) -#define GRUB_VIDEO_RENDER_TARGET_FRONT_BUFFER ((struct grub_video_render_target *) 0) -#define GRUB_VIDEO_RENDER_TARGET_BACK_BUFFER ((struct grub_video_render_target *) 1) +/* Predefined render target: */ +/* The render target that client code should render to. */ +#define GRUB_VIDEO_RENDER_TARGET_DISPLAY \ + ((struct grub_video_render_target *) 0) + /* Defined blitting formats. */ enum grub_video_blit_format { - /* Follow exactly field & mask information. */ - GRUB_VIDEO_BLIT_FORMAT_RGBA, - /* Make optimization assumption. */ - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8, - /* Follow exactly field & mask information. */ - GRUB_VIDEO_BLIT_FORMAT_RGB, - /* Make optimization assumption. */ - GRUB_VIDEO_BLIT_FORMAT_R8G8B8, + GRUB_VIDEO_BLIT_FORMAT_RGBA, /* General: Use fields & masks. */ + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_BGRA_8888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_RGB, /* General: Use fields & masks. */ + GRUB_VIDEO_BLIT_FORMAT_RGB_888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_BGR_888, /* Optimized format. */ /* When needed, decode color or just use value as is. */ - GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR + GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR, + /* Two color bitmap; bits packed: rows are not padded to byte boundary. */ + GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED }; /* Define blitting operators. */ @@ -127,6 +129,18 @@ /* What is location of reserved color bits. In Index Color mode, this is 0. */ unsigned int reserved_field_pos; + + /* For 1-bit bitmaps, the background color. Used for bits = 0. */ + grub_uint8_t bg_red; + grub_uint8_t bg_green; + grub_uint8_t bg_blue; + grub_uint8_t bg_alpha; + + /* For 1-bit bitmaps, the foreground color. Used for bits = 1. */ + grub_uint8_t fg_red; + grub_uint8_t fg_green; + grub_uint8_t fg_blue; + grub_uint8_t fg_alpha; }; struct grub_video_palette_data @@ -183,6 +197,10 @@ grub_err_t (*blit_glyph) (struct grub_font_glyph *glyph, grub_video_color_t color, int x, int y); + grub_err_t (*draw_string) (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y); + grub_err_t (*blit_bitmap) (struct grub_video_bitmap *bitmap, enum grub_video_blit_operators oper, int x, int y, int offset_x, int offset_y, @@ -255,6 +273,10 @@ grub_err_t grub_video_blit_glyph (struct grub_font_glyph *glyph, grub_video_color_t color, int x, int y); +grub_err_t grub_video_draw_string (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y); + grub_err_t grub_video_blit_bitmap (struct grub_video_bitmap *bitmap, enum grub_video_blit_operators oper, int x, int y, int offset_x, int offset_y, @@ -269,6 +291,9 @@ grub_err_t grub_video_scroll (grub_video_color_t color, int dx, int dy); +/* Swap the pages referred to by the front buffer and back buffer render + * targets, and the page previously referred to by the back buffer is made + * visible on the display. */ grub_err_t grub_video_swap_buffers (void); grub_err_t grub_video_create_render_target (struct grub_video_render_target **result, === added directory 'kern/generic' === added file 'kern/generic/get_time_ms.c' --- kern/generic/get_time_ms.c 1970-01-01 00:00:00 +0000 +++ kern/generic/get_time_ms.c 2008-07-04 16:55:48 +0000 @@ -0,0 +1,37 @@ +/* get_time_ms.c - generic time implementation -- using platform RTC. + * The generic implementation of these functions can be used for architectures + * or platforms that do not have a more specialized implementation. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include + +/* Calculate the time in milliseconds since the epoch based on the RTC. */ +grub_uint64_t +grub_get_time_ms (void) +{ + /* By dimensional analysis: + + 1000 ms N rtc ticks 1 s + ------- * ----------- * ----------- = 1000*N/T ms + 1 s 1 T rtc ticks + */ + grub_uint64_t ticks_ms_per_sec = ((grub_uint64_t) 1000) * grub_get_rtc (); + return grub_divmod64 (ticks_ms_per_sec, GRUB_TICKS_PER_SECOND, 0); +} === added file 'kern/generic/millisleep.c' --- kern/generic/millisleep.c 1970-01-01 00:00:00 +0000 +++ kern/generic/millisleep.c 2008-07-04 16:55:48 +0000 @@ -0,0 +1,39 @@ +/* millisleep.c - generic millisleep function. + * The generic implementation of these functions can be used for architectures + * or platforms that do not have a more specialized implementation. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 1999,2000,2001,2002,2003,2004,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include + +void +grub_millisleep (grub_uint32_t ms) +{ + grub_uint64_t start; + + start = grub_get_time_ms (); + + /* Instead of setting an end time and looping while the current time is + less than that, comparing the elapsed sleep time with the desired sleep + time handles the (unlikely!) case that the timer would wrap around + during the sleep. */ + + while (grub_get_time_ms () - start < ms) + grub_cpu_idle (); +} === modified file 'kern/i386/efi/init.c' --- kern/i386/efi/init.c 2007-10-22 18:59:33 +0000 +++ kern/i386/efi/init.c 2008-07-04 17:55:21 +0000 @@ -25,18 +25,13 @@ #include #include #include -#include - -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} +#include void grub_machine_init (void) { grub_efi_init (); + grub_tsc_calibrate (); } void === modified file 'kern/i386/pc/init.c' --- kern/i386/pc/init.c 2008-06-15 17:21:16 +0000 +++ kern/i386/pc/init.c 2008-07-04 18:03:26 +0000 @@ -30,6 +30,7 @@ #include #include #include +#include struct mem_region { @@ -46,12 +47,6 @@ grub_size_t grub_os_area_size; grub_size_t grub_lower_mem, grub_upper_mem; -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - void grub_arch_sync_caches (void *address __attribute__ ((unused)), grub_size_t len __attribute__ ((unused))) @@ -231,6 +226,8 @@ if (! grub_os_area_addr) grub_fatal ("no upper memory"); + + grub_tsc_calibrate (); } void === added file 'kern/i386/tsc.c' --- kern/i386/tsc.c 1970-01-01 00:00:00 +0000 +++ kern/i386/tsc.c 2008-07-04 17:55:21 +0000 @@ -0,0 +1,89 @@ +/* kern/i386/tsc.c - x86 TSC time source implementation + * Requires Pentium or better x86 CPU that supports the RDTSC instruction. + * This module uses the RTC (via grub_get_rtc()) to calibrate the TSC to + * real time. + * + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +/* Calibrated reference for TSC=0. This defines the time since the epoch in + milliseconds that TSC=0 refers to. */ +static grub_uint64_t tsc_boot_time; + +/* Calibrated TSC rate. (In TSC ticks per millisecond.) */ +static grub_uint64_t tsc_ticks_per_ms; + + +/* Declared in . */ +grub_uint64_t +grub_get_time_ms (void) +{ + return tsc_boot_time + grub_divmod64 (grub_get_tsc (), tsc_ticks_per_ms, 0); +} + + +/* How many RTC ticks to use for calibration loop. (>= 1) */ +#define CALIBRATION_TICKS 2 + +/* Calibrate the TSC based on the RTC. */ +void +grub_tsc_calibrate (void) +{ + /* First calbrate the TSC rate (relative, not absolute time). */ + grub_uint64_t start_tsc; + grub_uint64_t end_tsc; + grub_uint32_t initial_tick; + grub_uint32_t start_tick; + grub_uint32_t end_tick; + + /* Wait for the start of the next tick; + we'll base out timing off this edge. */ + initial_tick = grub_get_rtc (); + do + { + start_tick = grub_get_rtc (); + } + while (start_tick == initial_tick); + start_tsc = grub_get_tsc (); + + /* Wait for the start of the next tick. This will + be the end of the 1-tick period. */ + do + { + end_tick = grub_get_rtc (); + } + while (end_tick - start_tick < CALIBRATION_TICKS); + end_tsc = grub_get_tsc (); + + tsc_ticks_per_ms = + grub_divmod64 (grub_divmod64 + (end_tsc - start_tsc, end_tick - start_tick, 0) + * GRUB_TICKS_PER_SECOND, 1000, 0); + + /* Reference the TSC zero (boot time) to the epoch to + get an absolute real time reference. */ + grub_uint64_t ms_since_boot = grub_divmod64 (end_tsc, tsc_ticks_per_ms, 0); + grub_uint64_t mstime_now = grub_divmod64 ((grub_uint64_t) 1000 * end_tick, + GRUB_TICKS_PER_SECOND, + 0); + tsc_boot_time = mstime_now - ms_since_boot; +} === modified file 'kern/ieee1275/init.c' --- kern/ieee1275/init.c 2008-07-04 02:01:55 +0000 +++ kern/ieee1275/init.c 2008-07-04 19:43:25 +0000 @@ -47,12 +47,6 @@ extern char _end[]; void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - -void grub_exit (void) { grub_ieee1275_exit (); @@ -260,8 +254,8 @@ grub_console_fini (); } -grub_uint32_t -grub_get_rtc (void) +grub_uint64_t +grub_get_time_ms (void) { grub_uint32_t msecs = 0; @@ -270,6 +264,12 @@ return msecs; } +grub_uint32_t +grub_get_rtc (void) +{ + return grub_get_time_ms (); +} + grub_addr_t grub_arch_modules_addr (void) { === modified file 'kern/misc.c' --- kern/misc.c 2008-06-15 23:42:48 +0000 +++ kern/misc.c 2008-07-04 18:03:26 +0000 @@ -23,7 +23,6 @@ #include #include #include -#include void * grub_memmove (void *dest, const void *src, grub_size_t n) @@ -1018,17 +1017,6 @@ return p - dest; } -void -grub_millisleep_generic (grub_uint32_t ms) -{ - grub_uint32_t end_at; - - end_at = grub_get_rtc () + grub_div_roundup (ms * GRUB_TICKS_PER_SECOND, 1000); - - while (grub_get_rtc () < end_at) - grub_cpu_idle (); -} - /* Abort GRUB. This function does not return. */ void grub_abort (void) === modified file 'kern/sparc64/ieee1275/init.c' --- kern/sparc64/ieee1275/init.c 2007-10-22 18:59:33 +0000 +++ kern/sparc64/ieee1275/init.c 2008-07-04 17:55:21 +0000 @@ -66,12 +66,6 @@ /* Never reached. */ } -void -grub_millisleep (grub_uint32_t ms) -{ - grub_millisleep_generic (ms); -} - int grub_ieee1275_test_flag (enum grub_ieee1275_flag flag) { === 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-07-12 14:40:50 +0000 +++ util/i386/pc/grub-mkrescue.in 2008-07-19 21:39:24 +0000 @@ -71,7 +71,7 @@ --modules=*) modules=`echo "$option" | sed 's/--modules=//'` ;; --overlay=*) - overlay=`echo "$option" | sed 's/--overlay=//'` ;; + overlay=${overlay}${overlay:+ }`echo "$option" | sed 's/--overlay=//'` ;; --pkglibdir=*) input_dir=`echo "$option" | sed 's/--pkglibdir=//'` ;; --grub-mkimage=*) @@ -124,9 +124,10 @@ echo "insmod $i" done > ${aux_dir}/boot/grub/grub.cfg -if test "x$overlay" = x ; then : ; else - cp -dpR ${overlay}/* ${aux_dir}/ -fi +for d in ${overlay}; do + echo "Overlaying $d" + cp -vdpR ${d}/* ${aux_dir}/ +done if [ "x${image_type}" = xfloppy -o "x${emulation}" = xfloppy ] ; then # build memdisk === modified file 'video/bitmap.c' --- video/bitmap.c 2007-07-21 22:32:33 +0000 +++ video/bitmap.c 2008-07-03 13:49:18 +0000 @@ -78,7 +78,7 @@ switch (blit_format) { - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8: + case GRUB_VIDEO_BLIT_FORMAT_RGBA_8888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB | GRUB_VIDEO_MODE_TYPE_ALPHA; mode_info->bpp = 32; @@ -94,7 +94,7 @@ mode_info->reserved_field_pos = 24; break; - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8: + case GRUB_VIDEO_BLIT_FORMAT_RGB_888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB; mode_info->bpp = 24; mode_info->bytes_per_pixel = 3; === added file 'video/bitmap_scale.c' --- video/bitmap_scale.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,99 @@ +/* bitmap_scale.c - Bitmap scaling interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * This function creates a new scaled version of the bitmap SRC. The new + * bitmap has dimensions DST_WIDTH by DST_HEIGHT. The scaling algorithm + * is given by SCALE_METHOD. If an error is encountered, the return code is + * not equal to GRUB_ERR_NONE, and the bitmap DST is either not created, or + * it is destroyed before this function returns. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum grub_video_bitmap_scale_method + scale_method) +{ + + /* Verify the simplifying assumptions. */ + if (src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "null src bitmap in grub_video_bitmap_create_scaled"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "src format not supported for scale"); + if (src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + if (dst_width <= 0 || dst_height <= 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "requested to scale to a size w/ a zero dimension"); + if (src->mode_info.bytes_per_pixel * 8 != src->mode_info.bpp) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "bitmap to scale has inconsistent Bpp and bpp"); + + /* Create the new bitmap. */ + grub_err_t ret; + ret = grub_video_bitmap_create (dst, dst_width, dst_height, + src->mode_info.blit_format); + if (ret != GRUB_ERR_NONE) + return ret; /* Error. */ + + switch (scale_method) + { + case GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST: + ret = grub_video_bitmap_scale_nn (*dst, src); + break; + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR: + ret = grub_video_bitmap_scale_bilinear (*dst, src); + break; + default: + ret = grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid scale_method value"); + break; + } + + if (ret == GRUB_ERR_NONE) + { + /* Success: *dst is now a pointer to the scaled bitmap. */ + return GRUB_ERR_NONE; + } + else + { + /* Destroy the bitmap and return the error code. */ + grub_video_bitmap_destroy (*dst); + return ret; + } +} === added file 'video/bitmap_scale_bilinear.c' --- video/bitmap_scale_bilinear.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_bilinear.c 2008-07-03 14:28:15 +0000 @@ -0,0 +1,145 @@ +/* bitmap_scale_bilinear.c - Bilinear image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the bilinear interpolation algorithm + * to interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale func"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* If we have enough space to do so, use bilinear interpolation. + * Otherwise, fall back to nearest neighbor for this pixel. */ + if (sx < sw - 1 && sy < sh - 1) + { + /* Do bilinear interpolation. */ + + /* Fixed-point .8 numbers representing the fraction of the + * distance in the x (u) and y (v) direction within the + * box of 4 pixels in the source. */ + int u = (256 * sw * dx / dw) - (sx * 256); + int v = (256 * sh * dy / dh) - (sy * 256); + + for (comp = 0; comp < bytes_per_pixel; comp++) + { + /* Get the component's values for the + * 4 source corner pixels. */ + grub_uint8_t f00 = sptr[comp]; + grub_uint8_t f10 = sptr[comp + bytes_per_pixel]; + grub_uint8_t f01 = sptr[comp + sstride]; + grub_uint8_t f11 = sptr[comp + sstride + bytes_per_pixel]; + + /* Do linear interpolations along the top and bottom + * rows of the box. */ + grub_uint8_t f0y = (256 - v) * f00 / 256 + v * f01 / 256; + grub_uint8_t f1y = (256 - v) * f10 / 256 + v * f11 / 256; + + /* Interpolate vertically. */ + grub_uint8_t fxy = (256 - u) * f0y / 256 + u * f1y / 256; + + dptr[comp] = fxy; + } + } + else + { + /* Fall back to nearest neighbor interpolation. */ + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + } + return GRUB_ERR_NONE; +} === added file 'video/bitmap_scale_nn.c' --- video/bitmap_scale_nn.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_nn.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,109 @@ +/* bitmap_scale_nn.c - Nearest neighbor image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the nearest neighbor algorithm to + * interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale_nn"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + return GRUB_ERR_NONE; +} === modified file 'video/i386/pc/vbe.c' --- video/i386/pc/vbe.c 2008-01-21 15:48:27 +0000 +++ video/i386/pc/vbe.c 2008-07-09 20:59:34 +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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 8XckU4UJDYq38SA=