本篇主要解决两个问题。一是扩充上一篇内存管理部分的功能,实现一种按4KB大小分配内存的函数;二是解决鼠标显示中鼠标覆盖任务栏的问题。
1. 以4KB大小为单位分配内存的函数
如果每次分配内存都需要按字节去分配,效率还是比较低的,可用内存也会变得越来越零碎。这里作者引入一种以4KB大小为单位进行内存分配的函数。
首先要根据应用程序所需的内存大小,进行4KB向上取整,也就是不足4KB的部分用4KB补足。相应的代码如下:
unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size)
{
unsigned int a;
size = (size + 0xfff) & 0xfffff000;
a = memman_alloc(man, size);
return a;
}
int memman_free_4k(struct MEMMAN *man, unsigned int addr, unsigned int size)
{
int i;
size = (size + 0xfff) & 0xfffff000;
i = memman_free(man, addr, size);
return i;
}
可以看出对于4KB(0x1000)的向上取整,采用的方法是(size + 0xfff) & 0xfffff000
用十进制来举例子就很简单,比如以10为单位向上舍入,123舍入后就是130,这个很好理解。
2. 图层的叠加处理
引入了以4KB为单位进行内存分配的函数之后,本篇的重点是完成叠加处理。从鼠标和任务栏的叠加扩展开去,扩展到多个不同图层的叠加显示。这部分确实比较复杂,如果没明白的话,还是需要认认真真地多看几遍。
2.1 图层概念的引入
为了讲解叠加处理,需要引入图层的概念。这里作者引入的图层概念就是一张图片,除了有图案的地方,背景色是透明的。而多个窗口叠加在一起,可能是多个大小不同的图层的叠放。最上面的小图层用来描绘鼠标,下面依次是叠加在一起的不同图层,而最下面则是桌面背景。另外还需要实现窗口的移动。
首先考虑描述一个图层信息的数据结构:
struct SHEET {
unsigned char *buf;
int bxsize, bysize, vx0, vy0, col_inv, height, flags;
};
这个结构体中,buf用来存放图层上的图案内容。bxsize,bysize用于描述图层的大小,vx0和vy0用来描述图层在整个显示画面中的位置,col_inv表示图层的透明色色号,height表示图层的高度,flags用于存放有关图层设置的信息。
这样一个图层就可以通过这个结构体进行描述了。
当然,只有一个图层,我们无法实现叠加处理,还需要一个管理多个图层信息的结构体:
#define MAX_SHEETS 256
struct SHTCTL {
unsigned char *vram;
int xsize, ysize, top;
struct SHEET *sheets[MAX_SHEETS];
struct SHEET sheets0[MAX_SHEETS];
};
SHTCTL这个结构体用于管理图层。其中MAX_SHEETS是最大图层数,设置为256。结构体中,vram表示显存地址,xsize与ysize表示画面的大小,初始化时就从BOOTINFO中获取到并存起来。top用来表示最上面图层的高度。sheets0是一个结构体数组,用于保存图层的信息;而sheets是一个结构体指针数组。sheets0中存放的图层信息是乱序的,而通过sheets结构体指针数组将经过升序处理后的图层信息保存起来。
2.2 图层叠加处理
具备了这些数据结构之后,我们开始编写程序。首先需要对图层管理的数据结构进行初始化:
struct SHTCTL *shtctl_init(struct MEMMAN *memman, unsigned char *vram, int xsize, int ysize)
{
struct SHTCTL *ctl;
int i;
ctl = (struct SHTCTL *) memman_alloc_4k(memman, sizeof (struct SHTCTL));
if (ctl == 0) {
goto err;
}
ctl->vram = vram;
ctl->xsize = xsize;
ctl->ysize = ysize;
ctl->top = -1; /* 当前画面中没有显示的图层 */
for (i = 0; i < MAX_SHEETS; i++) {
ctl->sheets0[i].flags = 0; /* 图层标记为未使用 */
}
err:
return ctl;
}
这段程序的内容比较清楚。首先是通过内存分配函数给图层控制结构体分配足够的内存,通过sizeof(struct SHTCTL)来由编译器计算所需的内存大小。这个内存大小超过了4KB,所以用前面的内存分配函数来分配内存很合适。接下来将显存地址、画面大小等信息存放在结构体中。将top设置为-1,表示当前没有显示的图层;将sheets0数组中所有元素的flags成员置为0,表示当前图层都没有使用。
初始化之后,是分配空闲图层的函数:
#define SHEET_USE 1
struct SHEET *sheet_alloc(struct SHTCTL *ctl)
{
struct SHEET *sht;
int i;
for (i = 0; i < MAX_SHEETS; i++) {
if (ctl->sheets0[i].flags == 0) {
sht = &ctl->sheets0[i];
sht->flags = SHEET_USE; /* 将图层标记为使用 */
sht->height = -1; /* 表示当前图层隐藏 */
return sht;
}
}
return 0; /* 走到这里说明所有图层都处于使用状态,没有空闲图层 */
}
从图层管理结构体的sheets0数组中寻找未使用的图层,标记为使用,将height先设置为-1表示隐藏。
获取空闲图层之后,就可以根据实际情况对图层的参数进行设置,这也没什么难点。
void sheet_setbuf(struct SHEET *sht, unsigned char *buf, int xsize, int ysize, int col_inv)
{
sht->buf = buf;
sht->bxsize = xsize;
sht->bysize = ysize;
sht->col_inv = col_inv;
return;
}
下面是叠加显示中最关键的设置图层高度的函数,也是最复杂的:
void sheet_updown(struct SHTCTL *ctl, struct SHEET *sht, int height)
{
int h, old = sht->height; /* 存储图层原来位置的高度信息 */
/* 如果指定的高度过高或过低,则进行修正 */
if (height > ctl->top + 1) {
height = ctl->top + 1;
}
if (height < -1) {
height = -1;
}
sht->height = height; /* 设定图层高度 */
/* 以下通过sheets指针数组对图层进行重新排序 */
if (old > height) { /* 图层的新位置比原来低*/
if (height >= 0) {
/* 图层往下移动了,但还不是隐藏起来,这时把原位置和新位置之间的图层都向上升 */
for (h = old; h > height; h--) {
ctl->sheets[h] = ctl->sheets[h - 1];
ctl->sheets[h]->height = h;
}
ctl->sheets[height] = sht;
} else { /* 把图层隐藏起来 */
if (ctl->top > old) {
/* 图层的原位置不在最上层,则把图层原位置到最上层的图层依次向下降 */
for (h = old; h < ctl->top; h++) {
ctl->sheets[h] = ctl->sheets[h + 1];
ctl->sheets[h]->height = h;
}
}
ctl->top--; /* 隐藏了一个图层,显示的图层少了一个,最上面的图层高度下降 */
}
sheet_refresh(ctl); /* 根据更新后的图层信息,重新绘制画面 */
} else if (old < height) { /* 图层的新位置比原来的位置高 */
if (old >= 0) {
/* 图层原高度大于0,表示图层原来就处于显示状态,变更位置后,需要把原位置到新位置之间的图层都向下降 */
for (h = old; h < height; h++) {
ctl->sheets[h] = ctl->sheets[h + 1];
ctl->sheets[h]->height = h;
}
ctl->sheets[height] = sht;
} else { /* 图层原来处于隐藏状态,现在变成显示状态了 */
/* 把图层新位置与最上层之间的图层向上升 */
for (h = ctl->top; h >= height; h--) {
ctl->sheets[h + 1] = ctl->sheets[h];
ctl->sheets[h + 1]->height = h + 1;
}
ctl->sheets[height] = sht;
ctl->top++; /* 显示的图层增加了一个,最上面的图层高度也增加了 */
}
sheet_refresh(ctl); /* 根据更新后的图层信息,重新绘制画面 */
}
return;
}
这一段代码比较复杂。简单地理解,这段代码做的其实就是从一堆按顺序堆放的图层中抽出一张,改变其高度,再放回堆中,重新进行排序的过程。如果当前图层向下移动,那么处在该图层新老位置之间的图层要依次向上移动,填补该图层留出的空位,而总高度不变;如果该图层由显示变为隐藏,那么原来处在该图层上面的图层要依次向下移动,填补该图层留出的空位,这样总高度由于该图层的隐藏而减少了。
同理,如果图层向上移动,那么原来处于该图层新老位置之间的图层要依次向下移动,填补该图层留出的空位,而总高度不变;如果该图层由隐藏变为显示,那么原来处在该图层与最顶层之间的图层要依次向上移动,给该图层腾出空间,总高度由于该图层的显示而增加了。
如此解释,这段程序是否更好懂一些了呢?
排序之后,还需要从下到上依次描绘出各个图层的图案,最终最上层的图案会显示出来。这里就要用到sheet_refresh函数:
void sheet_refresh(struct SHTCTL *ctl)
{
int h, bx, by, vx, vy;
unsigned char *buf, c, *vram = ctl->vram;
struct SHEET *sht;
for (h = 0; h <= ctl->top; h++) {
sht = ctl->sheets[h];
buf = sht->buf;
for (by = 0; by < sht->bysize; by++) {
vy = sht->vy0 + by;
for (bx = 0; bx < sht->bxsize; bx++) {
vx = sht->vx0 + bx;
c = buf[by * sht->bxsize + bx];
if (c != sht->col_inv) {
vram[vy * ctl->xsize + vx] = c;
}
}
}
}
return;
}
从程序中可以看出,这个函数其实是将buf中不是透明色的像素点写入显存,这样自下而上层层写入下来,最终留下的就是最顶层的图案和下面未被覆盖的部分了。
叠加显示最重要的部分基本上就说完了,此外还有一些附加的函数,比如左右移动窗口的函数,释放图层的函数:
void sheet_slide(struct SHTCTL *ctl, struct SHEET *sht, int vx0, int vy0)
{
sht->vx0 = vx0;
sht->vy0 = vy0;
if (sht->height >= 0) { /* 当前图层正在显示 */
sheet_refresh(ctl); /* 根据新的图层信息重新描绘图案 */
}
return;
}
void sheet_free(struct SHTCTL *ctl, struct SHEET *sht)
{
if (sht->height >= 0) {
sheet_updown(ctl, sht, -1); /* 如果处于显示状态,先设置位隐藏 */
}
sht->flags = 0; /* 将状态设置为未使用 */
return;
}
接下来是修改主函数,在其中调用这部分功能了。相关程序如下:
struct SHTCTL *shtctl;
struct SHEET *sht_back, *sht_mouse;
unsigned char *buf_back, buf_mouse[256];
init_palette();
shtctl = shtctl_init(memman, binfo->vram, binfo->scrnx, binfo->scrny);
sht_back = sheet_alloc(shtctl);
sht_mouse = sheet_alloc(shtctl);
buf_back = (unsigned char *) memman_alloc_4k(memman, binfo->scrnx * binfo->scrny);
sheet_setbuf(sht_back, buf_back, binfo->scrnx, binfo->scrny, -1); /* 背景图层,没有透明色 */
sheet_setbuf(sht_mouse, buf_mouse, 16, 16, 99); /* 鼠标图层,设置透明色色号为99 */
init_screen8(buf_back, binfo->scrnx, binfo->scrny);
init_mouse_cursor8(buf_mouse, 99);
sheet_slide(shtctl, sht_back, 0, 0);
mx = (binfo->scrnx - 16) / 2; /*按照鼠标图层显示在画面中央来计算坐标 */
my = (binfo->scrny - 28 - 16) / 2;
sheet_slide(shtctl, sht_mouse, mx, my);
sheet_updown(shtctl, sht_back, 0);
sheet_updown(shtctl, sht_mouse, 1);
sprintf(s, "(%3d, %3d)", mx, my);
putfonts8_asc(buf_back, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
sprintf(s, "memory %dMB free : %dKB",
memtotal / (1024 * 1024), memman_total(memman) / 1024);
putfonts8_asc(buf_back, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
sheet_refresh(shtctl);
for (;;) {
io_cli();
if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
io_stihlt();
} else {
if (fifo8_status(&keyfifo) != 0) {
i = fifo8_get(&keyfifo);
io_sti();
sprintf(s, "%02X", i);
boxfill8(buf_back, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(buf_back, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
sheet_refresh(shtctl);
} else if (fifo8_status(&mousefifo) != 0) {
i = fifo8_get(&mousefifo);
io_sti();
if (mouse_decode(&mdec, i) != 0) {
sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
if ((mdec.btn & 0x01) != 0) {
s[1] = 'L';
}
if ((mdec.btn & 0x02) != 0) {
s[3] = 'R';
}
if ((mdec.btn & 0x04) != 0) {
s[2] = 'C';
}
boxfill8(buf_back, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
putfonts8_asc(buf_back, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
/* 移动鼠标 */
mx += mdec.x;
my += mdec.y;
if (mx < 0) {
mx = 0;
}
if (my < 0) {
my = 0;
}
if (mx > binfo->scrnx - 16) {
mx = binfo->scrnx - 16;
}
if (my > binfo->scrny - 16) {
my = binfo->scrny - 16;
}
sprintf(s, "(%3d, %3d)", mx, my);
boxfill8(buf_back, binfo->scrnx, COL8_008484, 0, 0, 79, 15); /* 清除原坐标显示 */
putfonts8_asc(buf_back, binfo->scrnx, 0, 0, COL8_FFFFFF, s); /* 显示新坐标 */
sheet_slide(shtctl, sht_mouse, mx, my); /* 包含了sheet_refresh */
}
}
}
}
主程序中当前还只有两个图层,一个是背景图层,另一个是鼠标图层。这样运行程序,移动鼠标时,就不会出现遮挡任务栏的情况了。
2.3 加快图层叠加处理速度
但是当前还存在着显示比较慢的问题,叠加处理的速度还可以进一步提升。
当前的鼠标图案是1616=256个像素,其实移动一次只要重新绘制256个像素点就可以了。但当前的实现是每次移动都会将整个画面共320200=64000个像素点重新描绘一遍,从这一点出发就可以提高速度:
void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1)
{
int h, bx, by, vx, vy;
unsigned char *buf, c, *vram = ctl->vram;
struct SHEET *sht;
for (h = 0; h <= ctl->top; h++) {
sht = ctl->sheets[h];
buf = sht->buf;
for (by = 0; by < sht->bysize; by++) {
vy = sht->vy0 + by;
for (bx = 0; bx < sht->bxsize; bx++) {
vx = sht->vx0 + bx;
if (vx0 <= vx && vx < vx1 && vy0 <= vy && vy < vy1) {
c = buf[by * sht->bxsize + bx];
if (c != sht->col_inv) {
vram[vy * ctl->xsize + vx] = c;
}
}
}
}
}
return;
}
可以看出sheet_refreshsub函数中通过vx0-vy1指定了范围,这样就可以只在鼠标的256个像素范围内重新描绘,速度得到提升。
sheet_slide函数修改如下:
void sheet_slide(struct SHTCTL *ctl, struct SHEET *sht, int vx0, int vy0)
{
int old_vx0 = sht->vx0, old_vy0 = sht->vy0;
sht->vx0 = vx0;
sht->vy0 = vy0;
if (sht->height >= 0) { /* 如果当前图层正在显示,按照新的图层信息重新描绘画面 */
sheet_refreshsub(ctl, old_vx0, old_vy0, old_vx0 + sht->bxsize, old_vy0 + sht->bysize);
sheet_refreshsub(ctl, vx0, vy0, vx0 + sht->bxsize, vy0 + sht->bysize);
}
return;
}
这里做的是记住移动前的显示位置,再设置新的位置,只需要将原位置的显示消除,在新位置描绘即可。
虽然不用将整个画面都描绘一遍,但还是通过if判断了画面内的所有位置。在描绘之前就限制好描绘的范围,减少不必要的if判断,可以进一步提高速度:
void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1)
{
int h, bx, by, vx, vy, bx0, by0, bx1, by1;
unsigned char *buf, c, *vram = ctl->vram;
struct SHEET *sht;
for (h = 0; h <= ctl->top; h++) {
sht = ctl->sheets[h];
buf = sht->buf;
/* vx0-vy1范围对bx0-by1的范围进行推断 */
bx0 = vx0 - sht->vx0;
by0 = vy0 - sht->vy0;
bx1 = vx1 - sht->vx0;
by1 = vy1 - sht->vy0;
if (bx0 < 0) { bx0 = 0; }
if (by0 < 0) { by0 = 0; }
if (bx1 > sht->bxsize) { bx1 = sht->bxsize; }
if (by1 > sht->bysize) { by1 = sht->bysize; }
for (by = by0; by < by1; by++) {
vy = sht->vy0 + by;
for (bx = bx0; bx < bx1; bx++) {
vx = sht->vx0 + bx;
c = buf[by * sht->bxsize + bx];
if (c != sht->col_inv) {
vram[vy * ctl->xsize + vx] = c;
}
}
}
}
return;
}
本篇从建立图层的数据结构出发,建立了图层动作的程序描述,并通过鼠标的实例进行实现。这部分从原理到程序内容都不少,看似简单实则又并不简单,自己就反复读了好几遍才理解。下一篇的制作窗口仍然与显示相关,敬请期待。