「30天自制操作系统」Day23 - 图形处理相关

编写 malloc

我们编写一个 api_malloc 函数,用来在应用程序中分配内存空间。

我们在编写应用程序的时候指定要分配的内存空间,在 bim2hrb 的时候指定,具体如下:

1
$(BIM2HRB) winhelo2.bim winhelo2.hrb 所需内存大小

当指定了 malloc 所需内存大小时,这个数值会和栈等的大小进行累加,并写入 .hrb 文件最开头的 4 个字节中。同时,malloc 用的内存空间在数据段中的开始位置,被保存在 .hrb 文件的 0x0020 处。

我们设计 API 如下:

MEMMAN 初始化:

寄存器
EDX 8
EBX memman 的地址
EAX memman 所管理的内存空间的起始地址
ECX memman所管理的内存空间的字节数

malloc:

寄存器
EDX 9
EBX memman 的地址
EAX 分配到的内存空间地址
ECX 需要请求的字节数

free:

寄存器
EDX 10
EBX memman 的地址
EAX 需要释放的内存空间地址
ECX 需要释放的字节数

API 编写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
_api_initmalloc:	; void api_initmalloc(void);
PUSH EBX
MOV EDX,8
MOV EBX,[CS:0x0020] ; malloc 内存空间的地址
MOV EAX,EBX
ADD EAX,32*1024 ; 加上 32KB
MOV ECX,[CS:0x0000] ; 数据段的大小
SUB ECX,EAX
INT 0x40
POP EBX
RET

_api_malloc: ; char *api_malloc(int size);
PUSH EBX
MOV EDX,9
MOV EBX,[CS:0x0020]
MOV ECX,[ESP+8] ; size
INT 0x40
POP EBX
RET

_api_free: ; void api_free(char *addr, int size);
PUSH EBX
MOV EDX,10
MOV EBX,[CS:0x0020]
MOV EAX,[ESP+ 8] ; addr
MOV ECX,[ESP+12] ; size
INT 0x40
POP EBX
RET

我们查看一下文件大小并运行一下:

可见这次我们仅仅用了 387 个字节就成功了~

画点

API 如下:

寄存器
EDX 11
EBX 窗口句柄
ESI 显示位置的 x 坐标
EDI 显示位置的 y 坐标
EAX 色号

hrb_api 中的处理以及汇编程序的编写(对寄存器值的处理)都与之前十分类似,就不贴代码了。我们通过画点 API 来实现画一个星空的功能,其实就是随机若干个坐标画点,程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_boxfilwin(int win, int x0, int y0, int x1, int y1, int col);
void api_initmalloc(void);
char *api_malloc(int size);
void api_point(int win, int x, int y, int col);
void api_end(void);

int rand(void);

void HariMain(void)
{
char *buf;
int win, i, x, y;
api_initmalloc();
buf = api_malloc(150 * 100);
win = api_openwin(buf, 150, 100, -1, "stars");
api_boxfilwin(win, 6, 26, 143, 93, 0);
for (i = 0; i < 50; i++) {
x = (rand() % 137) + 6;
y = (rand() % 67) + 26;
api_point(win, x, y, 3);
}
api_end();
}

看一下效果吧:

刷新窗口

为了防止刷新次数过多导致程序运行变慢,我们写一个刷新窗口的 API,定义如下:

寄存器
EDX 12
EBX 窗口句柄
EAX x0
ECX y0
ESI x1
EDI y1

然后我们在所有的窗口绘图命令中设置一个「不自动刷新」的选项,由于我们的 EBX 是 SHEET 的地址(偶数),可以设置在 EBX 为奇数的情况下不进行自动刷新。

画直线

画直线的方法很简单,设置好初始坐标、方向向量、长度即可。API 如下:

寄存器
EDX 13
EBX 窗口句柄
EAX x0
ECX y0
ESI x1
EDI y1
EBP 色号

这里注意对 dx 和 dy 的取值要好好把握,如果太稀疏会看起来像虚线,如果太紧凑会浪费 CPU 性能。

编写测试程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_initmalloc(void);
char *api_malloc(int size);
void api_refreshwin(int win, int x0, int y0, int x1, int y1);
void api_linewin(int win, int x0, int y0, int x1, int y1, int col);
void api_end(void);

void HariMain(void)
{
char *buf;
int win, i;
api_initmalloc();
buf = api_malloc(160 * 100);
win = api_openwin(buf, 160, 100, -1, "lines");
for (i = 0; i < 8; i++) {
api_linewin(win + 1, 8, 26, 77, i * 9 + 26, i);
api_linewin(win + 1, 88, 26, i * 9 + 88, 89, i);
}
api_refreshwin(win, 6, 26, 154, 90);
api_end();
}

我们来运行一下看看效果:

关闭窗口

我们来实现一个关闭窗口的 API:

寄存器
EDX 14
EBX 窗口句柄

这个很简单,我们通过 sheet_free 函数将其关闭掉就可以了。

键盘输入 API

要接受键盘输入,其实只要从和任务绑定的 FIFO 缓冲区中取出 1 个就可以了,注意等待键盘输入的时间要加上休眠功能。API 如下:

寄存器
EDX 15
EAX 输入的字符编码

若 EAX = 0 ,没有键盘输入时返回 -1,不休眠;若 EAX = 1,则休眠直到发送键盘输入。

这次的代码我们来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (edx == 15) {
for (;;) {
io_cli();
if (fifo32_status(&task->fifo) == 0) {
if (eax != 0) {
task_sleep(task); /* FIFO 为空,休眠并等待 */
} else {
io_sti();
reg[7] = -1;
return 0;
}
}
i = fifo32_get(&task->fifo);
io_sti();
if (i <= 1) { /* 光标用定时器 */
timer_init(cons->timer, &task->fifo, 1); /* 下次置为 1 */
timer_settime(cons->timer, 50);
}
if (i == 2) { /* 光标 ON */
cons->cur_c = COL8_FFFFFF;
}
if (i == 3) { /* 光标 OFF */
cons->cur_c = -1;
}
if (256 <= i && i <= 511) { /* 键盘数据 */
reg[7] = i - 256;
return 0;
}
}
}

这里我们用 cons->timer 代替了原来的 timer 变量。

我们改写一下应用程序,当我们按下回车的时候,窗口就关闭了,是我们预期的效果。

用键盘输入来消遣一下

通过键盘输入,我们可以用它来实现一个小程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void HariMain(void)
{
char *buf;
int win, i, x, y;
api_initmalloc();
buf = api_malloc(160 * 100);
win = api_openwin(buf, 160, 100, -1, "walk");
api_boxfilwin(win, 4, 24, 155, 95, 0 );
x = 76;
y = 56;
api_putstrwin(win, x, y, 3, 1, "*");
for (;;) {
i = api_getkey(1);
api_putstrwin(win, x, y, 0 , 1, "*");
if (i == '4' && x > 4) { x -= 8; }
if (i == '6' && x < 148) { x += 8; }
if (i == '8' && y > 24) { y -= 8; }
if (i == '2' && y < 80) { y += 8; }
if (i == 0x0a) { break; } /* 按回车键结束 */
api_putstrwin(win, x, y, 3, 1, "*");
}
api_closewin(win);
api_end();
}

通过按小键盘来实现上下左右移动,每次我们用黑色擦除掉之前的,用黄色画出当前的。效果如下:

强制结束并关闭窗口

现在如果我们按下 shift+F1,窗口并不会消失。也就是说我们强制结束的时候并没有执行 api_closewin。

为了解决这个问题,我们修改了结构体 SHEET,添加了一个 task 成员,用来存放该图层对应的任务。当应用程序结束时,查询所有的图层,如果图层的 task 为将要结束的应用程序任务,则关闭该图层。现在我们按下 shift+F1 强制结束程序,窗口也随之消失了!