# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080711163747-7v43uh66u2lp4mb7 # target_branch: file:///home/cdb/grub/repo/trunk-clean/ # testament_sha1: c4f86c7756386a7eb77e539f916c9f2820badc6b # timestamp: 2008-07-11 10:10:45 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080711163623-vpxrq241l75k5jed # # Begin patch === modified file '.bzrignore' --- .bzrignore 2008-07-03 04:15:16 +0000 +++ .bzrignore 2008-07-03 14:08:39 +0000 @@ -6,3 +6,5 @@ ./conf/powerpc-ieee1275.mk ./conf/sparc64-ieee1275.mk ./build* +./util/fonttool/build +./util/fonttool/fonttool.iws === modified file 'ChangeLog' --- ChangeLog 2008-07-07 14:11:06 +0000 +++ ChangeLog 2008-07-11 16:37:47 +0000 @@ -12,6 +12,73 @@ * disk/ata.c: Use named constants for status bits. +2008-07-04 Colin D Bennett + + High resolution timer support. Implemented for i386 CPU using TSC. + Extracted generic grub_millisleep() so it's linked in only as needed. + This requires a Pentium compatible CPU; currently the code does not + check for this (so it will fail on 386 and 486 machines). + + * conf/i386-efi.rmk: Added TSC high resolution time module, link with + generic grub_millisleep() function. + + * conf/i386-pc.rmk: Likewise. + + * conf/sparc64-ieee1275.rmk: Add kern/generic/millisleep.c and + kern/generic/get_time_ms.c to kernel, to use generic time functions. + + * conf/powerpc-ieee1275.rmk: Add kern/generic/millisleep.c to kernel, + to use generic grub_millisleep() function. + + * conf/i386-linuxbios.rmk: Added kern/generic/get_time_ms.c to the + kernel. + + * kern/generic/get_time_ms.c (grub_get_time_ms): New file. Platform + independent implementation of grub_get_time_ms() using the RTC that + can be linked into a platform's kernel when it does not implement its + own specialized grub_get_time_ms() function. + + * kern/generic/millisleep.c (grub_millisleep): New file. Extracted + from grub_millisleep_generic() in kern/misc.c and renamed. Changed it + to use grub_get_time_ms() instead of grub_get_rtc() for better + precision on when high resolution time is available. + + * kern/misc.c (grub_millisleep_generic): Deleted. Moved to + kern/generic/millisleep.c so that it is only included in the kernel + image when a platform does not define a specialized version. + + * commands/sleep.c (grub_interruptible_millisleep): Uses + grub_get_time_ms() instead of grub_get_rtc() to stay in sync with + grub_millisleep() from kern/generic/millisleep.c. + + * include/grub/i386/tsc.h (grub_get_tsc): New file. Inline function + grub_get_tsc() uses x86 RDTSC instruction (available on Pentium+ CPUs) + to read the counter value for the TSC. + (grub_tsc_calibrate): Declare this function for grub_machine_init(). + + * kern/i386/tsc.c (grub_get_time_ms): x86 TSC support providing a high + resolution clock. + (grub_tsc_calibrate): New function to calibrate the TSC using RTC. + + * include/grub/time.h (grub_get_time_ms): Added grub_get_time_ms() + function to return the current time in millseconds since the epoch. + This supports higher resolution time than grub_get_rtc() on some + platforms such as i386-pc, where the RTC has only about 1/18 s + precision but a higher precision timer such as the TSC is available. + + * kern/i386/efi/init.c (grub_millisleep): Deleted. Don't define + grub_millisleep() -- it just called grub_millisleep_generic() but now + it is linked to kern/generic/millisleep.c for the implementation. + + * kern/sparc64/ieee1275/init.c (grub_millisleep): Deleted. + + * kern/i386/pc/init.c (grub_machine_init): Call grub_tsc_calibrate(). + (grub_millisleep): Deleted. + + * kern/ieee1275/init.c (grub_millisleep): Deleted. + (grub_get_rtc): Now calls grub_get_time_ms(), which does the real + work. + 2008-07-04 Pavel Roskin * kern/i386/linuxbios/init.c (grub_machine_init): Cast addr to === modified file 'Makefile.in' --- Makefile.in 2008-07-02 18:03:23 +0000 +++ Makefile.in 2008-07-04 21:40:36 +0000 @@ -85,6 +85,39 @@ YACC = @YACC@ UNIFONT_HEX = @UNIFONT_HEX@ +### Pretty output control ### +# Set up compiler and linker commands that either is quiet (does not print +# the command line being executed) or verbose (print the command line). +_CC := $(CC) +_TARGET_CC := $(TARGET_CC) +_STRIP := $(STRIP) +_GENMODSRC := sh $(srcdir)/genmodsrc.sh +ifeq ($(V),1) + override V_PREFIX := + override CC = $(_CC) + override TARGET_CC = $(_CC) + override STRIP = $(_STRIP) + override GENMODSRC = $(_GENMODSRC) + override INFO_GENCMDLIST = + override INFO_GENFSLIST = + override INFO_GENPARTMAPLIST = + override INFO_GEN_FINAL_COMMAND_LIST = + override INFO_GEN_FINAL_FS_LIST = + override INFO_GEN_FINAL_PARTMAP_LIST = +else + override V_PREFIX := @ + override CC = @echo "COMPILE $<"; $(_CC) + override TARGET_CC = @echo "COMPILE(TARGET) $<"; $(_TARGET_CC) + override STRIP = @echo "STRIP $@"; $(_STRIP) + override GENMODSRC = @echo "GENMODSRC $@"; $(_GENMODSRC) + override INFO_GENCMDLIST = @echo "GENCMDLIST $@" + override INFO_GENFSLIST = @echo "GENFSLIST $@" + override INFO_GENPARTMAPLIST = @echo "GENPARTMAPLIST $@" + override INFO_GEN_FINAL_COMMAND_LIST = @echo "GENCMDLIST[final] $@" + override INFO_GEN_FINAL_FS_LIST = @echo "GENFSLIST[final] $@" + override INFO_GEN_FINAL_PARTMAP_LIST = @echo "GENPARTMAPLIST[final] $@" +endif + # Options. enable_grub_emu = @enable_grub_emu@ enable_grub_fstest = @enable_grub_fstest@ @@ -131,13 +164,16 @@ || (rm -f $@; exit 1) command.lst: $(COMMANDFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_COMMAND_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ fs.lst: $(FSFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_FS_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ partmap.lst: $(PARTMAPFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_PARTMAP_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ ifeq (, $(UNIFONT_HEX)) else === modified file 'commands/i386/pc/vbeinfo.c' --- commands/i386/pc/vbeinfo.c 2007-07-21 22:32:33 +0000 +++ commands/i386/pc/vbeinfo.c 2008-07-09 14:39:39 +0000 @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -48,12 +49,22 @@ grub_err_t err; char *modevar; - grub_printf ("List of compatible video modes:\n"); - err = grub_vbe_probe (&controller_info); if (err != GRUB_ERR_NONE) return err; + int row = 0; + + grub_printf ("VBE info: version: %d.%d OEM software rev: %d.%d\n", + controller_info.version >> 8, + controller_info.version & 0xFF, + controller_info.oem_software_rev >> 8, + controller_info.oem_software_rev & 0xFF); + row++; + grub_printf (" total memory: %d KiB\n", + (controller_info.total_memory << 16) / 1024); + row++; + /* Because the information on video modes is stored in a temporary place, it is better to copy it to somewhere safe. */ p = video_mode_list = real2pm (controller_info.video_mode_ptr); @@ -67,6 +78,10 @@ grub_memcpy (saved_video_mode_list, video_mode_list, video_mode_list_size); + grub_printf ("List of compatible video modes:\n"); + grub_printf ("Legend: P=Packed pixel, D=Direct color, " + "mask/pos=R/G/B/reserved\n"); + /* Walk through all video modes listed. */ for (p = saved_video_mode_list; *p != 0xFFFF; p++) { @@ -103,10 +118,10 @@ switch (mode_info_tmp.memory_model) { case 0x04: - memory_model = "Packed Pixel"; + memory_model = "Packed"; break; case 0x06: - memory_model = "Direct Color"; + memory_model = "Direct"; break; default: @@ -116,12 +131,31 @@ if (! memory_model) continue; - grub_printf ("0x%03x: %d x %d x %d bpp (%s)\n", - mode, + grub_printf ("0x%03x: %4dx%4dx%2d %s", + mode, mode_info_tmp.x_resolution, mode_info_tmp.y_resolution, mode_info_tmp.bits_per_pixel, - memory_model); + memory_model); + if (memory_model[0] == 'D') + grub_printf (" mask: %d/%d/%d/%d pos: %d/%d/%d/%d", + mode_info_tmp.red_mask_size, + mode_info_tmp.green_mask_size, + mode_info_tmp.blue_mask_size, + mode_info_tmp.rsvd_mask_size, + mode_info_tmp.red_field_position, + mode_info_tmp.green_field_position, + mode_info_tmp.blue_field_position, + mode_info_tmp.rsvd_field_position); + grub_printf ("\n"); + + if (++row >= 25) + { + grub_printf ("-- More --"); + grub_getkey (); + grub_printf ("\r \r"); + row = 0; + } } grub_free (saved_video_mode_list); === modified file 'commands/sleep.c' --- commands/sleep.c 2008-05-16 20:55:29 +0000 +++ commands/sleep.c 2008-07-04 16:55:48 +0000 @@ -43,15 +43,15 @@ grub_printf ("%d ", n); } -/* Based on grub_millisleep() from kern/misc.c. */ +/* Based on grub_millisleep() from kern/generic/millisleep.c. */ static int grub_interruptible_millisleep (grub_uint32_t ms) { - grub_uint32_t end_at; - - end_at = grub_get_rtc () + grub_div_roundup (ms * GRUB_TICKS_PER_SECOND, 1000); - - while (grub_get_rtc () < end_at) + grub_uint64_t start; + + start = grub_get_time_ms (); + + while (grub_get_time_ms () - start < ms) if (grub_checkkey () >= 0 && GRUB_TERM_ASCII_CHAR (grub_getkey ()) == GRUB_TERM_ESC) return 1; === modified file 'commands/videotest.c' --- commands/videotest.c 2007-07-21 22:32:33 +0000 +++ commands/videotest.c 2008-07-09 16: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-02 06:20:11 +0000 +++ conf/common.rmk 2008-07-03 14:12:08 +0000 @@ -330,7 +330,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-06-19 04:14:16 +0000 +++ conf/i386-efi.rmk 2008-07-03 04:19:16 +0000 @@ -84,7 +84,9 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/efi/init.c kern/parser.c kern/partition.c \ kern/env.c symlist.c kern/efi/efi.c kern/efi/init.c kern/efi/mm.c \ - term/efi/console.c disk/efi/efidisk.c + term/efi/console.c disk/efi/efidisk.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c kernel_mod_HEADERS = arg.h boot.h cache.h device.h disk.h dl.h elf.h elfload.h \ env.h err.h file.h fs.h kernel.h loader.h misc.h mm.h net.h parser.h \ partition.h pc_partition.h rescue.h symbol.h term.h time.h types.h \ === modified file 'conf/i386-linuxbios.rmk' --- conf/i386-linuxbios.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-linuxbios.rmk 2008-07-04 16:55:48 +0000 @@ -18,6 +18,7 @@ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/parser.c kern/partition.c \ kern/env.c \ + kern/generic/get_time_ms.c \ term/i386/pc/console.c \ term/i386/pc/at_keyboard.c term/i386/pc/vga_text.c \ symlist.c === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-06-19 04:14:16 +0000 +++ conf/i386-pc.rmk 2008-07-03 14:27:43 +0000 @@ -43,6 +43,8 @@ kern/disk.c kern/dl.c kern/file.c kern/fs.c kern/err.c \ kern/misc.c kern/mm.c kern/loader.c kern/rescue.c kern/term.c \ kern/i386/dl.c kern/i386/pc/init.c kern/parser.c kern/partition.c \ + kern/i386/tsc.c \ + kern/generic/millisleep.c \ kern/env.c \ term/i386/pc/console.c \ symlist.c @@ -256,7 +258,10 @@ videotest_mod_LDFLAGS = $(COMMON_LDFLAGS) # For bitmap.mod -bitmap_mod_SOURCES = video/bitmap.c +bitmap_mod_SOURCES = video/bitmap.c \ + video/bitmap_scale_nn.c \ + video/bitmap_scale_bilinear.c \ + video/bitmap_scale.c bitmap_mod_CFLAGS = $(COMMON_CFLAGS) bitmap_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'conf/powerpc-ieee1275.rmk' --- conf/powerpc-ieee1275.rmk 2008-06-19 04:14:16 +0000 +++ conf/powerpc-ieee1275.rmk 2008-07-03 04:19:16 +0000 @@ -85,6 +85,7 @@ kern/ieee1275/init.c term/ieee1275/ofconsole.c \ kern/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/parser.c kern/partition.c kern/env.c kern/powerpc/dl.c \ + kern/generic/millisleep.c \ symlist.c kern/powerpc/cache.S kernel_elf_HEADERS = grub/powerpc/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) === modified file 'conf/sparc64-ieee1275.rmk' --- conf/sparc64-ieee1275.rmk 2008-06-19 00:04:59 +0000 +++ conf/sparc64-ieee1275.rmk 2008-07-03 14:16:34 +0000 @@ -73,6 +73,7 @@ kern/rescue.c kern/term.c term/ieee1275/ofconsole.c \ kern/sparc64/ieee1275/openfw.c disk/ieee1275/ofdisk.c \ kern/partition.c kern/env.c kern/sparc64/dl.c symlist.c \ + kern/generic/millisleep.c kern/generic/get_time_ms.c \ kern/sparc64/cache.S kern/parser.c kernel_elf_HEADERS = grub/sparc64/ieee1275/ieee1275.h kernel_elf_CFLAGS = $(COMMON_CFLAGS) @@ -195,7 +196,7 @@ cat_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) === added file 'font/font.c' --- font/font.c 1970-01-01 00:00:00 +0000 +++ font/font.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,92 @@ +/* font.c - Font functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +grub_font_t +grub_font_get (const char *font_name) +{ + struct font_node *node; + + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + if (grub_strcmp (font->name, font_name) == 0) + return font; + } + + /* If no font by that name is found, return the first font in the list + * as a fallback. */ + return grub_font_list->value; +} + +const char * +grub_font_get_name (grub_font_t font) +{ + return font->name; +} + +int +grub_font_get_max_char_width (grub_font_t font) +{ + return font->max_char_width; +} + +int +grub_font_get_max_char_height (grub_font_t font) +{ + return font->max_char_height; +} + +int +grub_font_get_ascent (grub_font_t font) +{ + return font->ascent; +} + +int +grub_font_get_descent (grub_font_t font) +{ + return font->descent; +} + +int +grub_font_get_string_width (grub_font_t font, const char *str) +{ + int i; + int width; + struct grub_font_glyph *glyph; + grub_size_t len; + + len = grub_strlen (str); + + for (i = 0, width = 0; i < len; i++) + { + glyph = grub_font_get_glyph (font, str[i]); + width += glyph->device_width; + } + + return width; +} === added file 'font/font_cmd.c' --- font/font_cmd.c 1970-01-01 00:00:00 +0000 +++ font/font_cmd.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,75 @@ +/* font_cmd.c - Font command definition. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include + +static grub_err_t +loadfont_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc, + char **args) +{ + if (argc == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); + + while (argc--) + if (grub_font_load (*args++) != 0) + return GRUB_ERR_BAD_FONT; + + return GRUB_ERR_NONE; +} + +static grub_err_t +lsfonts_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + struct font_node *node; + + grub_printf ("Loaded fonts:\n"); + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + grub_printf ("%s\n", font->name); + } + + return GRUB_ERR_NONE; +} + +GRUB_MOD_INIT(font_manager) +{ + grub_font_loader_init (); + + grub_register_command ("loadfont", loadfont_command, GRUB_COMMAND_FLAG_BOTH, + "loadfont FILE...", + "Specify one or more font files to load.", 0); + + grub_register_command ("lsfonts", lsfonts_command, GRUB_COMMAND_FLAG_BOTH, + "lsfonts", + "List the loaded fonts.", 0); +} + +GRUB_MOD_FINI(font_manager) +{ + /* Should this free fonts, unknown_glyph, etc.? Freeing fonts could + * be a Bad Thing if there are still references to any of them. */ + + grub_unregister_command ("loadfont"); +} + === added file 'font/loader.c' --- font/loader.c 1970-01-01 00:00:00 +0000 +++ font/loader.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,696 @@ +/* loader.c - Functions to handle loading fonts and glyphs from files. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +//#include +//#include +#include +#include +#include + +/* Definition of font registry. */ +struct font_node *grub_font_list; + +static int +register_font (grub_font_t font); + +static void +free_font (grub_font_t font); + +static void +remove_font (grub_font_t font); + +struct font_file_section +{ + grub_file_t file; /* The file this section is in. */ + char name[4]; /* FOURCC name of the section. */ + grub_uint32_t length; /* Length of the section contents. */ + int eof; /* Set by open_section() on EOF. */ +}; + +/* Font file format constants. */ +static const char pff2_magic[4] = { 'P', 'F', 'F', '2' }; +static const char section_names_file[4] = { 'F', 'I', 'L', 'E' }; +static const char section_names_font_name[4] = { 'N', 'A', 'M', 'E' }; +static const char section_names_max_char_width[4] = { 'M', 'A', 'X', 'W' }; +static const char section_names_max_char_height[4] = { 'M', 'A', 'X', 'H' }; +static const char section_names_ascent[4] = { 'A', 'S', 'C', 'E' }; +static const char section_names_descent[4] = { 'D', 'E', 'S', 'C' }; +static const char section_names_char_index[4] = { 'C', 'H', 'I', 'X' }; +static const char section_names_data[4] = { 'D', 'A', 'T', 'A' }; + +/* Replace unknown glyphs with a rounded question mark. */ +static grub_uint8_t unknown_glyph_bitmap[] = +{ + /* 76543210 */ + 0x7C, /* ooooo */ + 0x82, /* o o */ + 0xBA, /* o ooo o */ + 0xAA, /* o o o o */ + 0xAA, /* o o o o */ + 0x8A, /* o o o */ + 0x9A, /* o oo o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x7C, /* ooooo */ + 0x00 /* */ +}; + +static struct grub_font_glyph *unknown_glyph; + +void +grub_font_loader_init (void) +{ + unknown_glyph = grub_malloc(sizeof(struct grub_font_glyph) + + sizeof(unknown_glyph_bitmap)); + if (! unknown_glyph) + return; + + unknown_glyph->width = 8; + unknown_glyph->height = 16; + unknown_glyph->offset_x = 0; + unknown_glyph->offset_y = 0; + unknown_glyph->device_width = 8; + grub_memcpy(unknown_glyph->bitmap, + unknown_glyph_bitmap, sizeof(unknown_glyph_bitmap)); +} + +/* + * Open the next section in the file. + * + * On success, the section name is stored in section->name and the length in + * section->length, and 0 is returned. On failure, 1 is returned and + * grub_errno is set approriately with an error message. + * + * If 1 is returned due to being at the end of the file, then section->eof is + * set to 1; otherwise, section->eof is set to 0. + */ +static int +open_section (grub_file_t file, struct font_file_section *section) +{ + grub_ssize_t retval; + grub_uint32_t raw_length; + + section->file = file; + section->eof = 0; + + /* Read the FOURCC section name. */ + retval = grub_file_read (file, section->name, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section name"); + return 1; /* Read error. */ + } + + /* Read the big-endian 32-bit section length. */ + retval = grub_file_read (file, (char *) &raw_length, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section length"); + return 1; /* Read error. */ + } + + /* Convert byte-order and store in *length. */ + section->length = grub_be_to_cpu32 (raw_length); + + return 0; +} + +/* Size in bytes of each character index (CHIX section) + * entry in the font file. */ +#define FONT_CHAR_INDEX_ENTRY_SIZE (4 + 1 + 4) + +/* + * Load the character index (CHIX) section contents from the font file. This + * presumes that the position of FILE is positioned immediately after the + * section length for the CHIX section (i.e., at the start of the section + * contents). Returns 0 upon success, nonzero for failure (in which case + * grub_errno is set appropriately). + */ +static int +load_font_index (grub_file_t file, grub_uint32_t sect_length, struct + grub_font *font) +{ + unsigned i; + +#if FONT_DEBUG >= 2 + grub_printf("load_font_index(sect_length=%d)\n", sect_length); +#endif + + /* Sanity check: ensure section length is divisible by the entry size. */ + if (sect_length % FONT_CHAR_INDEX_ENTRY_SIZE != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: character index length %d " + "is not a multiple of the entry size %d", + sect_length, FONT_CHAR_INDEX_ENTRY_SIZE); + return 1; /* Invalid index section length. */ + } + + /* Calculate the number of characters. */ + font->num_chars = sect_length / FONT_CHAR_INDEX_ENTRY_SIZE; + + /* Allocate the character index array. */ + font->char_index = grub_malloc (font->num_chars + * sizeof (struct char_index_entry)); + if (!font->char_index) + return 1; /* Error allocating memory. */ + +#if FONT_DEBUG >= 2 + grub_printf("num_chars=%d)\n", font->num_chars); +#endif + + /* Load the character index data from the file. */ + for (i = 0; i < font->num_chars; i++) + { + struct char_index_entry *entry = &font->char_index[i]; + + /* Read code point value; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->code, 4) != 4) + return 1; + entry->code = grub_be_to_cpu32 (entry->code); + + /* Read storage flags byte. */ + if (grub_file_read (file, (char *) &entry->storage_flags, 1) != 1) + return 1; + + /* Read glyph data offset; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->offset, 4) != 4) + return 1; + entry->offset = grub_be_to_cpu32 (entry->offset); + + /* No glyph loaded. Will be loaded on demand and cached thereafter. */ + entry->glyph = 0; + +#if FONT_DEBUG >= 5 + if (i < 10) /* Print the 1st 10 characters. */ + grub_printf("c=%d o=%d\n", entry->code, entry->offset); +#endif + } + + return 0; /* Index loaded OK. */ +} + +/* + * Read the contents of the specified section as a string, which is + * allocated on the heap. Returns 0 if there is an error. + */ +static char * +read_section_as_string (struct font_file_section *section) +{ + char *str; + grub_ssize_t ret; + + str = grub_malloc (section->length + 1); + if (!str) + return 0; + + ret = grub_file_read (section->file, str, section->length); + if (ret < 0 || ret != (grub_ssize_t) section->length) + { + grub_free (str); + return 0; + } + + str[section->length] = '\0'; + return str; +} + +/* + * Read the contents of the current section as a 16-bit integer value, + * which is stored into *VALUE. Returns 0 upon success, nonzero upon failure. + */ +static int +read_section_as_short (struct font_file_section *section, grub_int16_t *value) +{ + grub_uint16_t raw_value; + + if (section->length != 2) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: section %c%c%c%c length " + "is %d but should be 2", + section->name[0], section->name[1], + section->name[2], section->name[3], + section->length); + return 1; /* An error occurred. */ + } + if (grub_file_read (section->file, (char *) &raw_value, 2) != 2) + return 1; /* An error occurred. */ + + *value = grub_be_to_cpu16 (raw_value); + return 0; /* Successfully read the value. */ +} + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int +grub_font_load (const char *filename) +{ + grub_file_t file = 0; + struct font_file_section section; + char magic[4]; + grub_font_t font = 0; + +#if FONT_DEBUG >= 1 + grub_printf("add_font(%s)\n", filename); +#endif + + file = grub_file_open (filename); + if (!file) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("file opened\n"); +#endif + + /* Read the FILE section. It indicates the file format. */ + if (open_section (file, §ion) != 0) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("opened FILE section\n"); +#endif + if (grub_memcmp (section.name, section_names_file, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: 1st section must be FILE"); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section name ok\n"); +#endif + if (section.length != 4) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error (file type ID length is %d " + "but should be 4)", section.length); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section length ok\n"); +#endif + /* Check the file format type code. */ + if (grub_file_read (file, magic, 4) != 4) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("read magic ok\n"); +#endif + + if (grub_memcmp (magic, pff2_magic, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, "Invalid font magic %x %x %x %x", + magic[0], magic[1], magic[2], magic[3]); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("compare magic ok\n"); +#endif + + /* Allocate the font object. */ + font = (grub_font_t) grub_malloc (sizeof (struct grub_font)); + if (!font) + goto fail; + + font->file = file; + font->name = 0; + font->ascent = 0; + font->descent = 0; + font->max_char_width = 0; + font->max_char_height = 0; + font->num_chars = 0; + font->char_index = 0; + +#if FONT_DEBUG >= 3 + grub_printf("allocate font ok; loading font info\n"); +#endif + + /* Load the font information. */ + while (1) + { + if (open_section (file, §ion) != 0) + { + if (section.eof) + break; /* Done reading the font file. */ + else + goto fail; + } + +#if FONT_DEBUG >= 2 + grub_printf("opened section %c%c%c%c ok\n", + section.name[0], section.name[1], + section.name[2], section.name[3]); +#endif + + if (grub_memcmp (section.name, section_names_font_name, 4) == 0) + { + font->name = read_section_as_string (§ion); + if (!font->name) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_width, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_width) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_height, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_height) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_ascent, 4) == 0) + { + if (read_section_as_short (§ion, &font->ascent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_descent, 4) == 0) + { + if (read_section_as_short (§ion, &font->descent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_char_index, 4) == 0) + { + /* Load the font index. */ + if (load_font_index (file, section.length, font) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_data, 4) == 0) + { + /* When the DATA section marker is reached, we stop reading. */ + break; + } + else + { + /* Unhandled section type, simply skip past it. */ +#if FONT_DEBUG >= 3 + grub_printf("Unhandled section type, skipping.\n"); +#endif + if ((int) grub_file_seek (file, + grub_file_tell (file) + section.length) + == -1) + goto fail; + } + } + + if (!font->name) + { + grub_printf ("Note: Font has no name.\n"); + font->name = grub_strdup ("Unknown"); + } + +#if FONT_DEBUG >= 1 + grub_printf ("Loaded font `%s'.\n" + "Ascent=%d Descent=%d MaxW=%d MaxH=%d Number of characters=%d.\n", + font->name, + font->ascent, font->descent, + font->max_char_width, font->max_char_height, + font->num_chars); +#endif + + if (font->max_char_width == 0 + || font->max_char_height == 0 + || font->num_chars == 0 + || font->char_index == 0 + || font->ascent == 0 + || font->descent == 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Invalid font file: missing some required data."); + goto fail; + } + + /* Add the font to the global font registry. */ + if (register_font (font) != 0) + goto fail; + + return 0; /* Font loaded ok. */ + +fail: + free_font (font); + return 1; /* Failed to load font. */ +} + +/* + * Read a 16-bit big-endian integer from FILE, convert it to native byte + * order, and store it in *VALUE. + * Returns 0 on success, 1 on failure. + */ +static int +read_be_uint16 (grub_file_t file, grub_uint16_t * value) +{ + if (grub_file_read (file, (char *) value, 2) != 2) + return 1; + *value = grub_be_to_cpu16 (*value); + return 0; +} + +static int +read_be_int16 (grub_file_t file, grub_int16_t * value) +{ + /* For the signed integer version, use the same code as for unsigned. */ + return read_be_uint16 (file, (grub_uint16_t *) value); +} + +/* + * Return a pointer to the character index entry for the glyph corresponding to + * the codepoint CODE in the font FONT. If not found, return zero. + */ +static struct char_index_entry * +find_glyph (const grub_font_t font, grub_uint32_t code) +{ + grub_uint32_t i; + grub_uint32_t len = font->num_chars; + struct char_index_entry *table = font->char_index; + + /* Do a linear search. */ + for (i = 0; i < len; i++) + { + if (table[i].code == code) + return &table[i]; + } + + return 0; /* No entry found for code point CODE. */ +} + +static struct grub_font_glyph * +grub_font_get_glyph_internal (grub_font_t font, grub_uint32_t code) +{ + struct char_index_entry *index_entry; + + index_entry = find_glyph (font, code); + if (index_entry) + { + struct grub_font_glyph *glyph = 0; + grub_uint16_t width; + grub_uint16_t height; + grub_int16_t xoff; + grub_int16_t yoff; + grub_int16_t dwidth; + int len; + + if (index_entry->glyph) + return index_entry->glyph; /* Return cached glyph. */ + + /* Make sure we can find glyphs for error messages. Push active + error message to error stack and reset error message. */ + grub_error_push (); + + grub_file_seek (font->file, index_entry->offset); + + /* Read the glyph width, height, and baseline. */ + if (read_be_uint16(font->file, &width) != 0 + || read_be_uint16(font->file, &height) != 0 + || read_be_int16(font->file, &xoff) != 0 + || read_be_int16(font->file, &yoff) != 0 + || read_be_int16(font->file, &dwidth) != 0) + { + //remove_font (font); + return 0; + } + + len = (width * height + 7) / 8; + glyph = grub_malloc (sizeof (struct grub_font_glyph) + len); + if (! glyph) + { + //remove_font (font); + return 0; + } + + glyph->font = font; + glyph->width = width; + glyph->height = height; + glyph->offset_x = xoff; + glyph->offset_y = yoff; + glyph->device_width = dwidth; + + /* Don't try to read empty bitmaps (e.g., space characters). */ + if (len != 0) + { + if (grub_file_read (font->file, (char *) glyph->bitmap, len) != len) + { + //remove_font (font); + return 0; + } + } + + /* Restore old error message. */ + grub_error_pop (); + + /* Cache the glyph. */ + index_entry->glyph = glyph; + + return glyph; /* Glyph loaded ok. */ + } + + return 0; +} + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph * +grub_font_get_glyph (grub_font_t font, grub_uint32_t code) +{ + struct grub_font_glyph *glyph; + glyph = grub_font_get_glyph_internal (font, code); + if (glyph == 0) + glyph = unknown_glyph; + return glyph; +} + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph * +grub_font_get_glyph_any (grub_uint32_t code) +{ + struct font_node *node; + /* Keep track of next node, in case there's an I/O error in + * grub_font_get_glyph() and the font is removed from the list. */ + struct font_node *next; + + for (node = grub_font_list; node; node = next) + { + grub_font_t font; + struct grub_font_glyph *glyph; + + font = node->value; + next = node->next; + + glyph = grub_font_get_glyph_internal (font, code); + if (glyph) + return glyph; + } + + /* Uggh... Glyph was not found in any font. */ + return unknown_glyph; /* Failed to load glyph. */ +} + +/* + * Free the memory used by a font. + * This should not be called if the font has been made available to + * users (once it is added to the global font list), since there would + * be the possibility of a dangling pointer. + */ +static void +free_font (grub_font_t font) +{ + if (font) + { + if (font->file) + grub_file_close (font->file); + if (font->name) + grub_free (font->name); + if (font->char_index) + grub_free (font->char_index); + grub_free (font); + } +} + +/* + * Add FONT to the global font registry. + * Returns 0 upon success, nonzero on failure (the font was not registered). + */ +static int +register_font (grub_font_t font) +{ + struct font_node *node = 0; + + node = grub_malloc (sizeof (struct font_node)); + if (!node) + return 1; /* Error. */ + + node->value = font; + node->next = grub_font_list; + grub_font_list = node; + + return 0; /* Success. */ +} + +/* + * Remove the font from the global font list. We don't actually free the + * font's memory since users could be holding references to the font. + */ +static void +remove_font (grub_font_t font) +{ + struct font_node **nextp, *cur; + + for (nextp = &grub_font_list, cur = *nextp; + cur; + nextp = &cur->next, cur = cur->next) + { + if (cur->value == font) + { + *nextp = cur->next; + + /* Free the node, but not the font itself. */ + grub_free (cur); + + return; + } + } +} + === removed file 'font/manager.c' --- font/manager.c 2008-02-10 11:04:38 +0000 +++ font/manager.c 1970-01-01 00:00:00 +0000 @@ -1,282 +0,0 @@ -/* - * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2005,2006,2007 Free Software Foundation, Inc. - * - * GRUB is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * GRUB is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with GRUB. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -struct entry -{ - grub_uint32_t code; - grub_uint32_t offset; -}; - -struct font -{ - struct font *next; - grub_file_t file; - grub_uint32_t num; - struct entry table[0]; -}; - -static struct font *font_list; - -/* Fill unknown glyph's with rounded question mark. */ -static grub_uint8_t unknown_glyph[16] = -{ /* 76543210 */ - 0x7C, /* ooooo */ - 0x82, /* o o */ - 0xBA, /* o ooo o */ - 0xAA, /* o o o o */ - 0xAA, /* o o o o */ - 0x8A, /* o o o */ - 0x9A, /* o oo o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x7C, /* ooooo */ - 0x00 /* */ -}; - -static int -add_font (const char *filename) -{ - grub_file_t file = 0; - char magic[4]; - grub_uint32_t num, i; - struct font *font = 0; - - file = grub_file_open (filename); - if (! file) - goto fail; - - if (grub_file_read (file, magic, 4) != 4) - goto fail; - - if (grub_memcmp (magic, GRUB_FONT_MAGIC, 4) != 0) - { - grub_error (GRUB_ERR_BAD_FONT, "invalid font magic"); - goto fail; - } - - if (grub_file_read (file, (char *) &num, 4) != 4) - goto fail; - - num = grub_le_to_cpu32 (num); - font = (struct font *) grub_malloc (sizeof (struct font) - + sizeof (struct entry) * num); - if (! font) - goto fail; - - font->file = file; - font->num = num; - - for (i = 0; i < num; i++) - { - grub_uint32_t code, offset; - - if (grub_file_read (file, (char *) &code, 4) != 4) - goto fail; - - if (grub_file_read (file, (char *) &offset, 4) != 4) - goto fail; - - font->table[i].code = grub_le_to_cpu32 (code); - font->table[i].offset = grub_le_to_cpu32 (offset); - } - - font->next = font_list; - font_list = font; - - return 1; - - fail: - if (font) - grub_free (font); - - if (file) - grub_file_close (file); - - return 0; -} - -static void -remove_font (struct font *font) -{ - struct font **p, *q; - - for (p = &font_list, q = *p; q; p = &(q->next), q = q->next) - if (q == font) - { - *p = q->next; - - grub_file_close (font->file); - grub_free (font); - - break; - } -} - -/* Return the offset of the glyph corresponding to the codepoint CODE - in the font FONT. If no found, return zero. */ -static grub_uint32_t -find_glyph (const struct font *font, grub_uint32_t code) -{ - grub_uint32_t start = 0; - grub_uint32_t end = font->num - 1; - const struct entry *table = font->table; - - /* This shouldn't happen. */ - if (font->num == 0) - return 0; - - /* Do a binary search. */ - while (start <= end) - { - grub_uint32_t i = (start + end) / 2; - - if (table[i].code < code) - start = i + 1; - else if (table[i].code > code) - end = i - 1; - else - return table[i].offset; - } - - return 0; -} - -/* Set the glyph to something stupid. */ -static void -fill_with_default_glyph (grub_font_glyph_t glyph) -{ - unsigned i; - - /* Use pre-defined pattern to fill unknown glyphs. */ - for (i = 0; i < 16; i++) - glyph->bitmap[i] = unknown_glyph[i]; - - glyph->char_width = 1; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; -} - -/* Get a glyph corresponding to the codepoint CODE. Always fill glyph - information with something, even if no glyph is found. */ -int -grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph) -{ - struct font *font; - grub_uint8_t bitmap[32]; - - /* FIXME: It is necessary to cache glyphs! */ - - restart: - for (font = font_list; font; font = font->next) - { - grub_uint32_t offset; - - offset = find_glyph (font, code); - if (offset) - { - grub_uint32_t w; - int len; - - /* Make sure we can find glyphs for error messages. Push active - error message to error stack and reset error message. */ - grub_error_push (); - - grub_file_seek (font->file, offset); - if ((len = grub_file_read (font->file, (char *) &w, sizeof (w))) - != sizeof (w)) - { - remove_font (font); - goto restart; - } - - w = grub_le_to_cpu32 (w); - if (w != 1 && w != 2) - { - /* grub_error (GRUB_ERR_BAD_FONT, "invalid width"); */ - remove_font (font); - goto restart; - } - - if (grub_file_read (font->file, (char *) bitmap, w * 16) - != (grub_ssize_t) w * 16) - { - remove_font (font); - goto restart; - } - - /* Fill glyph with information. */ - grub_memcpy (glyph->bitmap, bitmap, w * 16); - - glyph->char_width = w; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; - - /* Restore old error message. */ - grub_error_pop (); - - return 1; - } - } - - /* Uggh... No font was found. */ - fill_with_default_glyph (glyph); - return 0; -} - -static grub_err_t -font_command (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ - if (argc == 0) - return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); - - while (argc--) - if (! add_font (*args++)) - return 1; - - return 0; -} - -GRUB_MOD_INIT(font_manager) -{ - grub_register_command ("font", font_command, GRUB_COMMAND_FLAG_BOTH, - "font FILE...", - "Specify one or more font files to display.", 0); -} - -GRUB_MOD_FINI(font_manager) -{ - grub_unregister_command ("font"); -} === modified file 'genmk.rb' --- genmk.rb 2008-07-02 18:03:23 +0000 +++ genmk.rb 2008-07-04 21:40:36 +0000 @@ -125,7 +125,7 @@ $(TARGET_CC) $(TARGET_CPPFLAGS) $(TARGET_CFLAGS) $(#{prefix}_CFLAGS) -c -o $@ $< #{mod_src}: moddep.lst genmodsrc.sh - sh $(srcdir)/genmodsrc.sh '#{mod_name}' $< > $@ || (rm -f $@; exit 1) + $(GENMODSRC) '#{mod_name}' $< > $@ || (rm -f $@; exit 1) ifneq ($(#{prefix}_EXPORTS),no) #{defsym}: #{pre_obj} @@ -157,18 +157,21 @@ PARTMAPFILES += #{partmap} #{command}: #{src} $(#{src}_DEPENDENCIES) gencmdlist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENCMDLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/gencmdlist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{fs}: #{src} $(#{src}_DEPENDENCIES) genfslist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENFSLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genfslist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{partmap}: #{src} $(#{src}_DEPENDENCIES) genpartmaplist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENPARTMAPLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genpartmaplist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) === added file 'include/grub/bitmap_scale.h' --- include/grub/bitmap_scale.h 1970-01-01 00:00:00 +0000 +++ include/grub/bitmap_scale.h 2008-07-03 14:27:43 +0000 @@ -0,0 +1,54 @@ +/* bitmap_scale.h - Bitmap scaling functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_BITMAP_SCALE_HEADER +#define GRUB_BITMAP_SCALE_HEADER 1 + +#include +#include +#include + +enum grub_video_bitmap_scale_method +{ + /* Choose the fastest interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST, + /* Choose the highest quality interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST, + /* Nearest neighbor interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST, + /* Bilinear interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR +}; + +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum + grub_video_bitmap_scale_method scale_method); + +#endif /* ! GRUB_BITMAP_SCALE_HEADER */ === modified file 'include/grub/font.h' --- include/grub/font.h 2007-07-21 22:32:33 +0000 +++ include/grub/font.h 2008-07-03 14:17:31 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2007 Free Software Foundation, Inc. + * Copyright (C) 2003,2007,2008 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,33 +21,85 @@ #include -#define GRUB_FONT_MAGIC "PPF\x7f" +/* Forward declaration of opaque structure grub_font. + * Users only pass struct grub_font pointers to the font module functions, + * and do not have knowledge of the structure contents. */ +struct grub_font; + +/* Font type used to access font functions. */ +typedef struct grub_font *grub_font_t; + struct grub_font_glyph { - /* Glyph width in pixels. */ - grub_uint8_t width; - - /* Glyph height in pixels. */ - grub_uint8_t height; - - /* Glyph width in characters. */ - grub_uint8_t char_width; - - /* Glyph baseline position in pixels (from up). */ - grub_uint8_t baseline; - - /* Glyph bitmap data array of bytes in ((width + 7) / 8) * height. - Bitmap is formulated by height scanlines, each scanline having - width number of pixels. Pixels are coded as bits, value 1 meaning - of opaque pixel and 0 is transparent. If width does not fit byte - boundary, it will be padded with 0 to make it fit. */ - grub_uint8_t bitmap[32]; + /* Reference to the font this glyph belongs to. */ + grub_font_t font; + + /* Glyph bitmap width in pixels. */ + grub_uint16_t width; + + /* Glyph bitmap height in pixels. */ + grub_uint16_t height; + + /* Glyph bitmap x offset in pixels. Add to screen coordinate. */ + grub_int16_t offset_x; + + /* Glyph bitmap y offset in pixels. Subtract from screen coordinate. */ + grub_int16_t offset_y; + + /* Number of pixels to advance to start the next character. */ + grub_uint16_t device_width; + + /* Row-major order, packed bits (no padding; rows can break within a byte). + * The length of the array is (width * height + 7) / 8. Within a + * byte, the most significant bit is the first (leftmost/uppermost) pixel. + * Pixels are coded as bits, value 1 meaning of opaque pixel and 0 is + * transparent. If the length of the array does not fit byte boundary, it + * will be padded with 0 bits to make it fit. */ + grub_uint8_t bitmap[0]; }; -typedef struct grub_font_glyph *grub_font_glyph_t; - -int grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph); + +/****** font/font.c ******/ + +/* Get the font that has the specified name. Font names are in the form + * "Family Name Bold Italic 14", where Bold and Italic are optional. + * If no font matches the name specified, the most recently loaded font + * is returned as a fallback. */ +grub_font_t grub_font_get (const char *font_name); + +const char *grub_font_get_name (grub_font_t font); + +int grub_font_get_max_char_width (grub_font_t font); + +int grub_font_get_max_char_height (grub_font_t font); + +int grub_font_get_ascent (grub_font_t font); + +int grub_font_get_descent (grub_font_t font); + +int grub_font_get_string_width (grub_font_t font, const char *str); + + +/****** font/loader.c ******/ + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int grub_font_load (const char *filename); + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph *grub_font_get_glyph (grub_font_t font, + grub_uint32_t code); + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph *grub_font_get_glyph_any (grub_uint32_t code); #endif /* ! GRUB_FONT_HEADER */ === added file 'include/grub/font_internal.h' --- include/grub/font_internal.h 1970-01-01 00:00:00 +0000 +++ include/grub/font_internal.h 2008-07-03 14:16:11 +0000 @@ -0,0 +1,71 @@ +/* font_internal.h - Font declarations for use internally by the font module. + * Users of the font module should not include this header. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_FONT_INTERNAL_HEADER +#define GRUB_FONT_INTERNAL_HEADER 1 + +#include +#include +#include +#include +#include +#include +#include + +#define FONT_DEBUG 0 + +struct char_index_entry +{ + grub_uint32_t code; + grub_uint8_t storage_flags; + grub_uint32_t offset; + struct grub_font_glyph *glyph; /* Glyph if loaded, or null. */ +}; + +struct grub_font +{ + char *name; + grub_file_t file; + short max_char_width; + short max_char_height; + short ascent; + short descent; + grub_uint32_t num_chars; + struct char_index_entry *char_index; +}; + +struct font_node +{ + struct font_node *next; + struct grub_font *value; +}; + +extern struct font_node *grub_font_list; + + +/****** loader.c ******/ + +/* Initialize the font loader module. */ +void +grub_font_loader_init (void); + + +#endif /* ! GRUB_FONT_INTERNAL_HEADER */ + === modified file 'include/grub/i386/pc/vbeblit.h' --- include/grub/i386/pc/vbeblit.h 2007-07-21 22:32:33 +0000 +++ include/grub/i386/pc/vbeblit.h 2008-07-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-04-18 15:42:57 +0000 +++ util/i386/pc/grub-mkrescue.in 2008-07-10 15:05:44 +0000 @@ -71,7 +71,7 @@ --modules=*) modules=`echo "$option" | sed 's/--modules=//'` ;; --overlay=*) - overlay=`echo "$option" | sed 's/--overlay=//'` ;; + overlay=${overlay}${overlay:+ }`echo "$option" | sed 's/--overlay=//'` ;; --pkglibdir=*) input_dir=`echo "$option" | sed 's/--pkglibdir=//'` ;; --grub-mkimage=*) @@ -124,9 +124,10 @@ echo "insmod $i" done > ${aux_dir}/boot/grub/grub.cfg -if test "x$overlay" = x ; then : ; else - cp -dpR ${overlay}/* ${aux_dir}/ -fi +for d in ${overlay}; do + echo "Overlaying $d" + cp -vdpR ${d}/* ${aux_dir}/ +done if [ "x${image_type}" = xfloppy -o "x${emulation}" = xfloppy ] ; then # build memdisk === modified file 'video/bitmap.c' --- video/bitmap.c 2007-07-21 22:32:33 +0000 +++ video/bitmap.c 2008-07-03 13:49:18 +0000 @@ -78,7 +78,7 @@ switch (blit_format) { - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8: + case GRUB_VIDEO_BLIT_FORMAT_RGBA_8888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB | GRUB_VIDEO_MODE_TYPE_ALPHA; mode_info->bpp = 32; @@ -94,7 +94,7 @@ mode_info->reserved_field_pos = 24; break; - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8: + case GRUB_VIDEO_BLIT_FORMAT_RGB_888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB; mode_info->bpp = 24; mode_info->bytes_per_pixel = 3; === added file 'video/bitmap_scale.c' --- video/bitmap_scale.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,99 @@ +/* bitmap_scale.c - Bitmap scaling interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * This function creates a new scaled version of the bitmap SRC. The new + * bitmap has dimensions DST_WIDTH by DST_HEIGHT. The scaling algorithm + * is given by SCALE_METHOD. If an error is encountered, the return code is + * not equal to GRUB_ERR_NONE, and the bitmap DST is either not created, or + * it is destroyed before this function returns. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum grub_video_bitmap_scale_method + scale_method) +{ + + /* Verify the simplifying assumptions. */ + if (src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "null src bitmap in grub_video_bitmap_create_scaled"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "src format not supported for scale"); + if (src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + if (dst_width <= 0 || dst_height <= 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "requested to scale to a size w/ a zero dimension"); + if (src->mode_info.bytes_per_pixel * 8 != src->mode_info.bpp) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "bitmap to scale has inconsistent Bpp and bpp"); + + /* Create the new bitmap. */ + grub_err_t ret; + ret = grub_video_bitmap_create (dst, dst_width, dst_height, + src->mode_info.blit_format); + if (ret != GRUB_ERR_NONE) + return ret; /* Error. */ + + switch (scale_method) + { + case GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST: + ret = grub_video_bitmap_scale_nn (*dst, src); + break; + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR: + ret = grub_video_bitmap_scale_bilinear (*dst, src); + break; + default: + ret = grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid scale_method value"); + break; + } + + if (ret == GRUB_ERR_NONE) + { + /* Success: *dst is now a pointer to the scaled bitmap. */ + return GRUB_ERR_NONE; + } + else + { + /* Destroy the bitmap and return the error code. */ + grub_video_bitmap_destroy (*dst); + return ret; + } +} === added file 'video/bitmap_scale_bilinear.c' --- video/bitmap_scale_bilinear.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_bilinear.c 2008-07-03 14:28:15 +0000 @@ -0,0 +1,145 @@ +/* bitmap_scale_bilinear.c - Bilinear image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the bilinear interpolation algorithm + * to interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale func"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* If we have enough space to do so, use bilinear interpolation. + * Otherwise, fall back to nearest neighbor for this pixel. */ + if (sx < sw - 1 && sy < sh - 1) + { + /* Do bilinear interpolation. */ + + /* Fixed-point .8 numbers representing the fraction of the + * distance in the x (u) and y (v) direction within the + * box of 4 pixels in the source. */ + int u = (256 * sw * dx / dw) - (sx * 256); + int v = (256 * sh * dy / dh) - (sy * 256); + + for (comp = 0; comp < bytes_per_pixel; comp++) + { + /* Get the component's values for the + * 4 source corner pixels. */ + grub_uint8_t f00 = sptr[comp]; + grub_uint8_t f10 = sptr[comp + bytes_per_pixel]; + grub_uint8_t f01 = sptr[comp + sstride]; + grub_uint8_t f11 = sptr[comp + sstride + bytes_per_pixel]; + + /* Do linear interpolations along the top and bottom + * rows of the box. */ + grub_uint8_t f0y = (256 - v) * f00 / 256 + v * f01 / 256; + grub_uint8_t f1y = (256 - v) * f10 / 256 + v * f11 / 256; + + /* Interpolate vertically. */ + grub_uint8_t fxy = (256 - u) * f0y / 256 + u * f1y / 256; + + dptr[comp] = fxy; + } + } + else + { + /* Fall back to nearest neighbor interpolation. */ + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + } + return GRUB_ERR_NONE; +} === added file 'video/bitmap_scale_nn.c' --- video/bitmap_scale_nn.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_nn.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,109 @@ +/* bitmap_scale_nn.c - Nearest neighbor image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the nearest neighbor algorithm to + * interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale_nn"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + return GRUB_ERR_NONE; +} === modified file 'video/i386/pc/vbe.c' --- video/i386/pc/vbe.c 2008-01-21 15:48:27 +0000 +++ video/i386/pc/vbe.c 2008-07-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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 L0eLqd1LZKCbPINk//4u5IpwoSDbTHYS