mirror of
https://github.com/limine-bootloader/limine
synced 2024-12-23 22:36:48 +03:00
limine: Add support for multiple terminals and properly document it
This commit is contained in:
parent
15f111f5c3
commit
fb175747df
191
PROTOCOL.md
191
PROTOCOL.md
@ -19,6 +19,8 @@ languages.
|
||||
All pointers are 64-bit wide. All pointers point to the object with the
|
||||
higher half direct map offset already added to them, unless otherwise noted.
|
||||
|
||||
The calling convention matches the SysV C ABI for the specific architecture.
|
||||
|
||||
### Executable formats
|
||||
|
||||
The Limine protocol does not enforce any specific executable format, but
|
||||
@ -306,7 +308,7 @@ ID:
|
||||
|
||||
Request:
|
||||
```c
|
||||
typedef void (*limine_terminal_callback)(uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
typedef void (*limine_terminal_callback)(struct limine_terminal *, uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
|
||||
struct limine_terminal_request {
|
||||
uint64_t id[4];
|
||||
@ -320,22 +322,201 @@ struct limine_terminal_request {
|
||||
|
||||
Response:
|
||||
```c
|
||||
typedef void (*limine_terminal_write)(const char *, uint64_t);
|
||||
|
||||
struct limine_terminal_response {
|
||||
uint64_t revision;
|
||||
uint64_t terminal_count;
|
||||
struct limine_terminal **terminals;
|
||||
};
|
||||
```
|
||||
|
||||
* `terminal_count` - How many terminals are present.
|
||||
* `terminals` - Pointer to an array of `terminal_count` pointers to
|
||||
`struct limine_terminal` structures.
|
||||
|
||||
```c
|
||||
typedef void (*limine_terminal_write)(const char *, uint64_t);
|
||||
|
||||
struct limine_terminal {
|
||||
uint32_t columns;
|
||||
uint32_t rows;
|
||||
struct limine_framebuffer *framebuffer;
|
||||
limine_terminal_write write;
|
||||
};
|
||||
```
|
||||
|
||||
* `columns` and `rows` - Columns and rows provided by the terminal.
|
||||
* `framebuffer` - The framebuffer associated with this terminal.
|
||||
* `write` - Physical pointer to the terminal write() function.
|
||||
The function is not thread-safe, nor reentrant, per-terminal.
|
||||
This means multiple terminals may be called simultaneously, and multiple
|
||||
callbacks may be handled simultaneously.
|
||||
|
||||
Note: Omitting this request will cause the bootloader to not initialise
|
||||
the terminal service. The terminal is further documented in the stivale2
|
||||
specification.
|
||||
the terminal service.
|
||||
|
||||
#### Terminal callback
|
||||
|
||||
The callback is a function that is part of the kernel, which is called by the
|
||||
terminal during a `write()` call whenever an event or escape sequence cannot
|
||||
be handled by the bootloader's terminal alone, and the kernel may want to be
|
||||
notified in order to handle it itself.
|
||||
|
||||
Returning from the callback will resume the `write()` call which will return
|
||||
to its caller normally.
|
||||
|
||||
Not returning from a callback may leave the terminal in an undefined state
|
||||
and cause issues.
|
||||
|
||||
The callback function has the following prototype:
|
||||
```c
|
||||
void callback(struct limine_terminal *terminal, uint64_t type, uint64_t, uint64_t, uint64_t);
|
||||
```
|
||||
|
||||
The `terminal` argument is a pointer to the Limine terminal structure which
|
||||
has the `write()` call that caused the callback.
|
||||
|
||||
The purpose of the last 3 arguments changes depending on the `type` argument.
|
||||
|
||||
The callback types are as follows:
|
||||
|
||||
* `LIMINE_TERMINAL_CB_DEC` - (type value: `10`)
|
||||
|
||||
This callback is triggered whenever a DEC Private Mode (DECSET/DECRST)
|
||||
sequence is encountered that the terminal cannot handle alone. The arguments
|
||||
to this callback are: `terminal`, `type`, `values_count`, `values`, `final`.
|
||||
|
||||
`values_count` is a count of how many values are in the array pointed to by
|
||||
`values`. `values` is a pointer to an array of `uint32_t` values, which are
|
||||
the values passed to the DEC private escape.
|
||||
`final` is the final character in the DEC private escape sequence (typically
|
||||
`l` or `h`).
|
||||
|
||||
* `LIMINE_TERMINAL_CB_BELL` - (type value: `20`)
|
||||
|
||||
This callback is triggered whenever a bell event is determined to be
|
||||
necessary (such as when a bell character `\a` is encountered). The arguments
|
||||
to this callback are: `terminal`, `type`, `unused1`, `unused2`, `unused3`.
|
||||
|
||||
* `LIMINE_TERMINAL_CB_PRIVATE_ID` - (type value: `30`)
|
||||
|
||||
This callback is triggered whenever the kernel has to respond to a DEC
|
||||
private identification request. The arguments to this callback are:
|
||||
`terminal`, `type`, `unused1`, `unused2`, `unused3`.
|
||||
|
||||
* `LIMINE_TERMINAL_CB_STATUS_REPORT` - (type value `40`)
|
||||
|
||||
This callback is triggered whenever the kernel has to respond to a ECMA-48
|
||||
status report request. The arguments to this callback are: `terminal`,
|
||||
`type`, `unused1`, `unused2`, `unused3`.
|
||||
|
||||
* `LIMINE_TERMINAL_CB_POS_REPORT` - (type value `50`)
|
||||
|
||||
This callback is triggered whenever the kernel has to respond to a ECMA-48
|
||||
cursor position report request. The arguments to this callback are:
|
||||
`terminal`, `type`, `x`, `y`, `unused3`. Where `x` and `y` represent the
|
||||
cursor position at the time the callback is triggered.
|
||||
|
||||
* `LIMINE_TERMINAL_CB_KBD_LEDS` - (type value `60`)
|
||||
|
||||
This callback is triggered whenever the kernel has to respond to a keyboard
|
||||
LED state change request. The arguments to this callback are: `terminal`,
|
||||
`type`, `led_state`, `unused2`, `unused3`. `led_state` can have one of the
|
||||
following values: `0, 1, 2, or 3`. These values mean: clear all LEDs, set
|
||||
scroll lock, set num lock, and set caps lock LED, respectively.
|
||||
|
||||
* `LIMINE_TERMINAL_CB_MODE` - (type value: `70`)
|
||||
|
||||
This callback is triggered whenever an ECMA-48 Mode Switch sequence
|
||||
is encountered that the terminal cannot handle alone. The arguments to this
|
||||
callback are: `terminal`, `type`, `values_count`, `values`, `final`.
|
||||
|
||||
`values_count` is a count of how many values are in the array pointed to by
|
||||
`values`. `values` is a pointer to an array of `uint32_t` values, which are
|
||||
the values passed to the mode switch escape.
|
||||
`final` is the final character in the mode switch escape sequence (typically
|
||||
`l` or `h`).
|
||||
|
||||
* `LIMINE_TERMINAL_CB_LINUX` - (type value `80`)
|
||||
|
||||
This callback is triggered whenever a private Linux escape sequence
|
||||
is encountered that the terminal cannot handle alone. The arguments to this
|
||||
callback are: `terminal`, `type`, `values_count`, `values`, `unused3`.
|
||||
|
||||
`values_count` is a count of how many values are in the array pointed to by
|
||||
`values`. `values` is a pointer to an array of `uint32_t` values, which are
|
||||
the values passed to the Linux private escape.
|
||||
|
||||
#### Terminal context control
|
||||
|
||||
The `write()` function can additionally be used to set and restore terminal
|
||||
context, and refresh the terminal fully.
|
||||
|
||||
In order to achieve this, special values for the `length` argument are
|
||||
passed. These values are:
|
||||
```c
|
||||
#define LIMINE_TERMINAL_CTX_SIZE ((uint64_t)(-1))
|
||||
#define LIMINE_TERMINAL_CTX_SAVE ((uint64_t)(-2))
|
||||
#define LIMINE_TERMINAL_CTX_RESTORE ((uint64_t)(-3))
|
||||
#define LIMINE_TERMINAL_FULL_REFRESH ((uint64_t)(-4))
|
||||
```
|
||||
|
||||
For `CTX_SIZE`, the `ptr` variable has to point to a location to which the
|
||||
terminal will *write* a single `uint64_t` which contains the size of the
|
||||
terminal context.
|
||||
|
||||
For `CTX_SAVE` and `CTX_RESTORE`, the `ptr` variable has to point to a
|
||||
location to which the terminal will *save* or *restore* its context from,
|
||||
respectively.
|
||||
This location must have a size congruent to the value received from
|
||||
`CTX_SIZE`.
|
||||
|
||||
For `FULL_REFRESH`, the `ptr` variable is unused. This routine is to be used
|
||||
after control of the framebuffer is taken over and the bootloader's terminal
|
||||
has to *fully* repaint the framebuffer to avoid inconsistencies.
|
||||
|
||||
#### x86_64
|
||||
|
||||
Additionally, the kernel must ensure, when calling `write()`, that:
|
||||
|
||||
* Either the GDT provided by the bootloader is still properly loaded, or a
|
||||
custom GDT is loaded with at least the following descriptors in this specific
|
||||
order:
|
||||
|
||||
- Null descriptor
|
||||
- 16-bit code descriptor. Base = `0`, limit = `0xffff`. Readable.
|
||||
- 16-bit data descriptor. Base = `0`, limit = `0xffff`. Writable.
|
||||
- 32-bit code descriptor. Base = `0`, limit = `0xffffffff`. Readable.
|
||||
- 32-bit data descriptor. Base = `0`, limit = `0xffffffff`. Writable.
|
||||
- 64-bit code descriptor. Base and limit irrelevant. Readable.
|
||||
- 64-bit data descriptor. Base and limit irrelevant. Writable.
|
||||
|
||||
* The currently loaded virtual address space is still the one provided at
|
||||
entry by the bootloader, or a custom virtual address space is loaded which
|
||||
identity maps the framebuffer memory region associated with the terminal, and
|
||||
all the bootloader reclaimable memory regions, with read, write, and execute
|
||||
permissions.
|
||||
|
||||
* The routine is called *by its physical address* (the value of the function
|
||||
pointer is already physical), which should be identity mapped.
|
||||
|
||||
* Bootloader-reclaimable memory entries are left untouched until after the
|
||||
kernel is done utilising bootloader-provided facilities (this terminal being
|
||||
one of them).
|
||||
|
||||
Notes regarding segment registers and FPU:
|
||||
|
||||
The values of the FS and GS segments are guaranteed preserved across the
|
||||
call. All other segment registers may have their "hidden" portion
|
||||
overwritten, but Limine guarantees that the "visible" portion is going to
|
||||
be restored to the one used at the time of call before returning.
|
||||
|
||||
No registers other than the segment registers and general purpose registers
|
||||
are going to be used. Especially, this means that there is no need to save
|
||||
and restore FPU, SSE, or AVX state when calling the terminal write function.
|
||||
|
||||
#### Terminal characteristics
|
||||
|
||||
The terminal should strive for Linux console compatibility.
|
||||
|
||||
### Framebuffer Feature
|
||||
|
||||
|
@ -292,7 +292,8 @@ void (*term_context_save)(uint64_t ptr);
|
||||
void (*term_context_restore)(uint64_t ptr);
|
||||
void (*term_full_refresh)(void);
|
||||
|
||||
void (*term_callback)(uint64_t, uint64_t, uint64_t, uint64_t) = NULL;
|
||||
uint64_t term_arg = 0;
|
||||
void (*term_callback)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t) = NULL;
|
||||
|
||||
struct term_context term_context;
|
||||
|
||||
@ -669,7 +670,11 @@ static void dec_private_parse(uint8_t c) {
|
||||
}
|
||||
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_DEC, esc_values_i, (uintptr_t)esc_values, c);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_DEC, esc_values_i, (uintptr_t)esc_values, c);
|
||||
} else {
|
||||
term_callback(TERM_CB_DEC, esc_values_i, (uintptr_t)esc_values, c, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -679,7 +684,11 @@ static void linux_private_parse(void) {
|
||||
}
|
||||
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_LINUX, esc_values_i, (uintptr_t)esc_values, 0);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_LINUX, esc_values_i, (uintptr_t)esc_values, 0);
|
||||
} else {
|
||||
term_callback(TERM_CB_LINUX, esc_values_i, (uintptr_t)esc_values, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -705,7 +714,11 @@ static void mode_toggle(uint8_t c) {
|
||||
}
|
||||
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_MODE, esc_values_i, (uintptr_t)esc_values, c);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_MODE, esc_values_i, (uintptr_t)esc_values, c);
|
||||
} else {
|
||||
term_callback(TERM_CB_MODE, esc_values_i, (uintptr_t)esc_values, c, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -819,7 +832,11 @@ static void control_sequence_parse(uint8_t c) {
|
||||
break;
|
||||
case 'c':
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_PRIVATE_ID, 0, 0, 0);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_PRIVATE_ID, 0, 0, 0);
|
||||
} else {
|
||||
term_callback(TERM_CB_PRIVATE_ID, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'd':
|
||||
@ -849,19 +866,31 @@ static void control_sequence_parse(uint8_t c) {
|
||||
switch (esc_values[0]) {
|
||||
case 5:
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_STATUS_REPORT, 0, 0, 0);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_STATUS_REPORT, 0, 0, 0);
|
||||
} else {
|
||||
term_callback(TERM_CB_STATUS_REPORT, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_POS_REPORT, x + 1, y + 1, 0);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_POS_REPORT, x + 1, y + 1, 0);
|
||||
} else {
|
||||
term_callback(TERM_CB_POS_REPORT, x + 1, y + 1, 0, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'q':
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_KBD_LEDS, esc_values[0], 0, 0);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_KBD_LEDS, esc_values[0], 0, 0);
|
||||
} else {
|
||||
term_callback(TERM_CB_KBD_LEDS, esc_values[0], 0, 0, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'J':
|
||||
@ -1066,7 +1095,11 @@ is_csi:
|
||||
break;
|
||||
case 'Z':
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_PRIVATE_ID, 0, 0, 0);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_PRIVATE_ID, 0, 0, 0);
|
||||
} else {
|
||||
term_callback(TERM_CB_PRIVATE_ID, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '(':
|
||||
@ -1185,7 +1218,11 @@ void term_putchar(uint8_t c) {
|
||||
case '\a':
|
||||
// The bell is handled by the kernel
|
||||
if (term_callback != NULL) {
|
||||
term_callback(TERM_CB_BELL, 0, 0, 0);
|
||||
if (term_arg != 0) {
|
||||
term_callback(term_arg, TERM_CB_BELL, 0, 0, 0);
|
||||
} else {
|
||||
term_callback(TERM_CB_BELL, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case 14:
|
||||
|
@ -103,7 +103,8 @@ extern void (*term_full_refresh)(void);
|
||||
#define TERM_CTX_RESTORE ((uint64_t)(-3))
|
||||
#define TERM_FULL_REFRESH ((uint64_t)(-4))
|
||||
|
||||
extern void (*term_callback)(uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
extern uint64_t term_arg;
|
||||
extern void (*term_callback)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
|
||||
extern bool term_autoflush;
|
||||
|
||||
|
@ -108,7 +108,7 @@ static void *_get_request(uint64_t id[4]) {
|
||||
extern symbol stivale2_term_write_entry;
|
||||
extern void *stivale2_rt_stack;
|
||||
extern uint64_t stivale2_term_callback_ptr;
|
||||
void stivale2_term_callback(uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
void stivale2_term_callback(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
#endif
|
||||
|
||||
bool limine_load(char *config, char *cmdline) {
|
||||
@ -552,6 +552,7 @@ FEAT_END
|
||||
struct fb_info fb;
|
||||
|
||||
// Terminal feature
|
||||
uint64_t *term_fb_ptr = NULL;
|
||||
FEAT_START
|
||||
struct limine_terminal_request *terminal_request = get_request(LIMINE_TERMINAL_REQUEST);
|
||||
if (terminal_request == NULL) {
|
||||
@ -561,6 +562,8 @@ FEAT_START
|
||||
struct limine_terminal_response *terminal_response =
|
||||
ext_mem_alloc(sizeof(struct limine_terminal_response));
|
||||
|
||||
struct limine_terminal *terminal = ext_mem_alloc(sizeof(struct limine_terminal));
|
||||
|
||||
quiet = false;
|
||||
serial = false;
|
||||
|
||||
@ -579,18 +582,28 @@ FEAT_START
|
||||
term_callback = (void *)terminal_request->callback;
|
||||
#endif
|
||||
|
||||
term_arg = reported_addr(terminal);
|
||||
|
||||
#if defined (__i386__)
|
||||
if (stivale2_rt_stack == NULL) {
|
||||
stivale2_rt_stack = ext_mem_alloc(16384) + 16384;
|
||||
}
|
||||
|
||||
terminal_response->write = (uintptr_t)(void *)stivale2_term_write_entry;
|
||||
terminal->write = (uintptr_t)(void *)stivale2_term_write_entry;
|
||||
#elif defined (__x86_64__)
|
||||
terminal_response->write = (uintptr_t)term_write;
|
||||
terminal->write = (uintptr_t)term_write;
|
||||
#endif
|
||||
|
||||
terminal_response->columns = term_cols;
|
||||
terminal_response->rows = term_rows;
|
||||
term_fb_ptr = &terminal->framebuffer;
|
||||
|
||||
terminal->columns = term_cols;
|
||||
terminal->rows = term_rows;
|
||||
|
||||
uint64_t *term_list = ext_mem_alloc(1 * sizeof(uint64_t));
|
||||
term_list[0] = reported_addr(terminal);
|
||||
|
||||
terminal_response->terminal_count = 1;
|
||||
terminal_response->terminals = reported_addr(term_list);
|
||||
|
||||
terminal_request->response = reported_addr(terminal_response);
|
||||
|
||||
@ -621,6 +634,10 @@ skip_fb_init:;
|
||||
// For now we only support 1 framebuffer
|
||||
struct limine_framebuffer *fbp = ext_mem_alloc(sizeof(struct limine_framebuffer));
|
||||
|
||||
if (term_fb_ptr != NULL) {
|
||||
*term_fb_ptr = reported_addr(fbp);
|
||||
}
|
||||
|
||||
struct edid_info_struct *edid_info = get_edid_info();
|
||||
if (edid_info != NULL) {
|
||||
fbp->edid_size = sizeof(struct edid_info_struct);
|
||||
|
@ -73,7 +73,7 @@
|
||||
extern symbol stivale2_term_write_entry;
|
||||
void *stivale2_rt_stack = NULL;
|
||||
uint64_t stivale2_term_callback_ptr = 0;
|
||||
void stivale2_term_callback(uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
void stivale2_term_callback(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
#endif
|
||||
|
||||
bool stivale2_load(char *config, char *cmdline) {
|
||||
|
@ -43,6 +43,7 @@ bits 64
|
||||
mov rsi, [rbp + 16]
|
||||
mov rdx, [rbp + 24]
|
||||
mov rcx, [rbp + 32]
|
||||
mov r8, [rbp + 40]
|
||||
|
||||
call .get_got
|
||||
.get_got:
|
||||
|
@ -39,6 +39,7 @@ bits 64
|
||||
mov rsi, [rbp + 16]
|
||||
mov rdx, [rbp + 24]
|
||||
mov rcx, [rbp + 32]
|
||||
mov r8, [rbp + 40]
|
||||
|
||||
mov rbx, rsp
|
||||
mov rsp, [user_stack]
|
||||
|
16
limine.h
16
limine.h
@ -169,14 +169,22 @@ struct limine_framebuffer_request {
|
||||
#define LIMINE_TERMINAL_CTX_RESTORE ((uint64_t)(-3))
|
||||
#define LIMINE_TERMINAL_FULL_REFRESH ((uint64_t)(-4))
|
||||
|
||||
struct limine_terminal;
|
||||
|
||||
typedef void (*limine_terminal_write)(const char *, uint64_t);
|
||||
typedef void (*limine_terminal_callback)(uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
typedef void (*limine_terminal_callback)(struct limine_terminal *, uint64_t, uint64_t, uint64_t, uint64_t);
|
||||
|
||||
struct limine_terminal {
|
||||
uint32_t columns;
|
||||
uint32_t rows;
|
||||
LIMINE_PTR(struct limine_framebuffer *) framebuffer;
|
||||
LIMINE_PTR(limine_terminal_write) write;
|
||||
};
|
||||
|
||||
struct limine_terminal_response {
|
||||
uint64_t revision;
|
||||
uint32_t columns;
|
||||
uint32_t rows;
|
||||
LIMINE_PTR(limine_terminal_write) write;
|
||||
uint64_t terminal_count;
|
||||
LIMINE_PTR(struct limine_terminal **) terminals;
|
||||
};
|
||||
|
||||
struct limine_terminal_request {
|
||||
|
@ -138,7 +138,7 @@ extern char kernel_start[];
|
||||
|
||||
static void limine_main(void) {
|
||||
if (_terminal_request.response) {
|
||||
stivale2_print = _terminal_request.response->write;
|
||||
stivale2_print = _terminal_request.response->terminals[0]->write;
|
||||
}
|
||||
|
||||
e9_printf("\nWe're alive");
|
||||
@ -323,9 +323,14 @@ FEAT_START
|
||||
}
|
||||
struct limine_terminal_response *term_response = _terminal_request.response;
|
||||
e9_printf("Terminal feature, revision %d", term_response->revision);
|
||||
e9_printf("Columns: %d", term_response->columns);
|
||||
e9_printf("Rows: %d", term_response->rows);
|
||||
e9_printf("Write function at: %x", term_response->write);
|
||||
e9_printf("%d terminal(s)", term_response->terminal_count);
|
||||
for (size_t i = 0; i < term_response->terminal_count; i++) {
|
||||
struct limine_terminal *terminal = term_response->terminals[i];
|
||||
e9_printf("Columns: %d", terminal->columns);
|
||||
e9_printf("Rows: %d", terminal->rows);
|
||||
e9_printf("Using framebuffer: %x", terminal->framebuffer);
|
||||
e9_printf("Write function at: %x", terminal->write);
|
||||
}
|
||||
FEAT_END
|
||||
|
||||
for (;;);
|
||||
|
Loading…
Reference in New Issue
Block a user