| 1 | ||
| 1 | ||
| 1 | ||
| 1 | ||
| 1 |
Kodos to anyone who gets the thumbnail.
I've continued making progress on my operating system. Everything is still fairly basic. I fixed a pretty basic bug that was holding me back. And I just took an extra second to confirm I got reasonably gas lit.
I switched from using integer types like, unsigned int, unsigned long int, unsigned long long int; to u32, u64. And I feel very vindicated now.
In types.h I had written.
typedef unsigned long u64;
I always thought that was 64 bit. After compiling for both Linux and my system it looks like that is 64 bit on Linux, and 32 bit on mine. Which meant my timing functions were wrong.
u64 get_cpu_cycles() {
u32 lo, hi;
asm volatile ("rdtscp" : "=a"(lo), "=d"(hi));
return ((u64)hi<<32 | lo;
}
When your u64 is lying as is actually 32 bits that doesn't work. So I needed:
typedef unsigned long long int u64;
Ridiculous. And I'm so glad I don't have to type that out just to access a 64 bit number. I always find it funny how many convenience things get added to languages that aren't convenient at all.
I was wondering why every demo I created to try building a scheduler was glitching out. And I confirmed that I wasn't crazy with the following code.
First off, we have a new itoa function. That's integer to ascii.
char *itoa(u32 num,char *out) {
if(!out) out=(char*)malloc(30);
u32 i=0;
while(num) { //Print number backwards
char o=num%10;
out[i]=o+48;
num/=10;
++i;
}
for(u32 j=0;j<i/2;++j) { //Reverse the string
char tmp=out[j];
out[j]=out[i-j-1];
}
out[i]='\0'; //End the string with null termination
return out;
}
Next we needed to make a few upgrades to some existing functions. Let's expand our memset functions. We went from this:
void memset_int(u32 *mem,u32 value,u32 size) {
for(u32 i=0;i<size;++i) {
mem[i]=value;
}
}
To this:
void memset_int(u32 *mem,u32 value,u32 size) {
for(u32 i=0;i<size;++i) {
mem[i]=value;
}
}
void memset_char(char *mem,char value,u32 size) {
for(u32 i=0;i<size;++i) {
mem[i]=value;
}
}
void __attribute((overloadable)) memset(u32 *mem,u32 value,u32 size) {
for(u32 i=0;i<size;++i) {
mem[i]=value;
}
}
void __attribute((overloadable)) memset(char *mem,char value,u32 size) {
for(u32 i=0;i<size;++i) {
mem[i]=value;
}
}
void __attribute((overloadable)) memset(u64 *mem,u64 value,u32 size) {
for(u32 i=0;i<size;++i) {
mem[i]=value;
}
}
Then our super basic print function for VGA text mode was missing a feature (likely many). Went from this:
void print(char *msg) {
unsigned short *text_buffer = (unsigned short *)0xB8000;
for (u32 i = 0; msg[i] != '\0'; i++) {
text_buffer[i] = (unsigned short)msg[i] | ((unsigned short)0x0E << 8); // Yellow text
}
}
to this
void print(char *msg) {
unsigned short *text_buffer = (unsigned short *)0xB8000;
u32 x=0;
u32 y=0;
u32 width=80;
for (u32 i = 0; msg[i] != '\0'; i++) {
if(msg[i]=='\n') {
x=0;
++y;
continue;
}
text_buffer[y*width+x] = (unsigned short)msg[i] | ((unsigned short)0x0E << 8); // Yellow text
++x;
}
}
Now we can make a demo program to print the sizeof value in the OS.
void kernel_main(multiboot_info *info) {
memset(buff,'\n',0);
itoa(sizeof(char),buff);
print(buff);
memset(buff,'\n',1);
itoa(sizeof(unsigned int),buff+1);
print(buff);
memset(buff,'\n',2);
itoa(sizeof(unsigned long int),buff+2);
print(buff);
memset(buff,'\n',3);
itoa(sizeof(unsigned long long int),buff+3);
print(buff);
memset(buff,'\n',4);
itoa(sizeof(u32),buff+4);
print(buff);
memset(buff,'\n',5);
itoa(sizeof(u64),buff+5);
print(buff);
halt();
}
The result after running make qemu is:
1
4
4
8
4
8
u64 would have produced 4 before the fix.
And the linux program to show I'm not insane to think "unsigned long int" and "unsigned long long int" is redundant everywhere else I've ever coded.
#include <stdio.h>
#include "types.h"
void main() {
printf("%d\n%d\n%d\n%d\n%d\n%d\n",
sizeof(char),
sizeof(unsigned int),
sizeof(unsigned long int),
sizeof(unsigned long long int),
sizeof(u32),
sizeof(u64));
}
We get
1
4
8
8
4
8
I'm a big believer that a good environments have a primitive kind of value they really hammer in on as their primary data type. Bash uses the string. Javascript the dictionary. Erlang the destructuring tuple. Python the multi-dimensional array. Uiua is the stack. For most of C it's the pointer (internally int or basically <type>*). I think languages, or environments in general go wrong when they don't target a particular data type. It should be obvious that for mine u32 is the common type. So much so we also have this:
typedef unsigned int var;
Just so we can be more like Javascript. Speaking of Javascript. Time sharing and the scheduler. The new big feature. Given we don't have userland, or process isolation, or context switching (except for the new smart sleeps), and we are single threaded, but I want an easy way for more than one thing to happen at a time, and it's called responsive OS where we don't want waiting around to be a significant thing... An event queue like Javascript makes a lot of sense.
Originally I was going to keep separate the event queue and a scheduling table. The scheduling table was going to hold records for future events that would be created by things like setTimeout(&func,&data,ms) or setInterval(...). And those would create work events when they matured. But then how to model the difference between one time event emitters and repeat event emitters. But actually this can collapse into a single struct; the events, the timeouts, and the intervals.
typedef void* (*EventCallback)(u64 cycles, void *data, void *state);
typedef struct __attribute__((packed)) {
u32 active;
u64 target_cycle;
u64 repeat_interval;
EventCallback callback;
void *data;
} EventQueue;
Then it's not so complicated from there. A holey ring-buffer pushes events on. We check if the event is active (not consumed) and that the current cycle is passed the target cycle. If so we call the callback and give it its enjoined data if it has some. Before we call that callback, we set active to 0. Unless it has a non-zero repeat_interval in which case we set the target_cycle. Actually in that case we decrement active by one, which helps us add a feature for setting an interval with a maximum count for it to fire. In the case they call the standard function without that and want it to run indefinitely we set it to U32_MAX which will let it run 4 billion times. That or I can check that it equals U32_MAX and not decrement it.
The callbacks also has a state parameter and return value for functional nerds. Maybe it's redundant because someone could modify the memory at *data. But what functional programmers do with their cloaked state values is none of my business. If you want multiple callbacks to share a replaceable state value that you also don't want to be global, I'm not here to ask questions. The important thing is that functional smut is something global state doesn't need to worry about.
Time to wrap things up. I wanted to share about improved sleep functions, new image and video types, conversion between image formats, image and video scaling, and broken bilinear and trilinear upscaling for image and video respectively that just hasn't been working.
Maybe I'll get that working well and get some other scaling methods in and that will be the next thing to write about.
We have enough that making a game of snake is pretty simple. The most critical and obvious thing missing at the moment is a bitmap font. It's damn near impossible to print anything in VGA graphics mode right now (framebuffer). Right now I'm launching it in QEMU if I want to print something, and rebooting and running it on metal if I want to see graphics. But I can't do both at the same time.
I'm currently on the lookout for archaic but backwards comparable hardware graphics acceleration. Hoping to find something. Then I can port a top down doom game I made a while ago that used opengl without writing every individual pixel manually. Also want to get in image rotation.
Comment preview