This is the development log of Snowdrop OS. It was developed primarily during evenings, some longer and some shorter. Outside of this, I often spent time thinking about what to do next and how to do it.
Re-familiarized myself with boot loaders and BIOS booting process.
Read real-mode interrupt primer.
Start thinking about organization of segments (stack, etc.).
Start thinking about where applications will be loaded, and how they'll return to the OS.
Installed Bochs, a virtual machine provider which I hope to use as my development environment.
Put together toolchain - it assembles and creates boot loader floppy image from one script.
The floppy imaging tool tries to be extra smart and injects its own bootloader. dd'ing the image to insert my own bootloader simply corrupts things.
For now, I can either create a floppy image with files on it, or a bootable, empty floppy image.
About five different tools later, I can boot, as well as have valid files on the floppy image.
The problem could be that I'm missing the BPB boilerplate. I added it, and it boots in Bochs, but I can't read the FS from a DOS 6.22 VM. However, in VMWare, I can read the FS, but of course, doesn't boot.
It turns out I was missing one byte from the beginning of my BPB (BIOS Parameter Block). Once I added it, my floppy disk image booted in both Bochs and VMWare. It also became readable in both DOS and Windows XP. That one byte took 3 hours to find.
Of course, as soon as I try to copy even one file to the floppy image, it stops working (can't be read by DOS or Windows XP).
I reached a compromise where DOS no longer can read the floppy, but Windows XP can. Of course, it boots in both Bochs and VMWare.
I refused to give up and took a look at an online example of a bootloader which specifies the PBP in order to make a "proper" floppy image. While the FAT12 specification says that the first 3 bytes of the boot sector are ignored, this is not true. With a 0 there, DOS 6.22 doesn't recognize the floppy as valid. Instead, I changed the 0 to a nop (0x90) instruction, and it started working. I don't know if this is a DOS-specific thing, or not, but the generated floppy image now boots in Bochs and VMWare, and is recognized as a valid floppy disk in both Windows XP and DOS.
The build script now injects a few test files into the floppy image.
Started reading about reading from a floppy disk, since the plan is to read FAT12 and locate a file with a specific name which will contain the kernel.
Wrote some calls to help me debug FAT12 operations.
I don't know if I will be able to find a suitable debugger for Windows which will work with Bochs, so I'm going to code up some functions to dump registry values.
Found a good LBA-to-CHS translation routine and incorporated it in the code. I've tested it using a converter utility website. Looks like it's working well, but I'd like to clean it up again. The point of this routine is to be able to map a logical disk sector number (0 to n-1) to a physical Cylinder-Head-Sector tuple which is required by the BIOS's interrupt 13h (disk services). The ultimate purpose is for the loader to load the kernel.
The simple "print hex byte to screen" routine is already paying off.
TODO: Clean up LBS-to-CHS code (save results of division)
TODO: Make sure head/cylinder calculation makes total sense before moving on
I have a feeling I'll run out of space with the boot loader, so I'm refactoring the LBA-to-CHS function.
Saved 4 bytes by switching to using the stack rather than temporary registers.
LBA-to-CHS head/cylinder calculation makes sense fully now, and I've tested it using an online converter.
Saved 2 bytes from the LBA-to-CHS routine. This was possible because there's no point in preserving the ax register, since it will be set in preparation for the int 13h call anyway, right after returning from the LBA-to-CHS routine.
The code is now debug printing first three entries in the root directory.
Wrote the fragment which finds the root directory entry which belongs to the kernel file. The boot loader file size is getting uncomfortably close to 512 bytes. It will lose a bit of "weight" once I remove some of the text output functions. Found a damned bug, where if a file has the kernel file name, but a wrong last character, the code considers it the kernel.
The bug was fixed shortly after. Tested that kernel file is found regardless of whether there are other files around it on the floppy.
TODO: Convert the debug_print function to work without a character count (by expecting 0-terminated strings). Still allow character count to be specified, to dump chunks of memory more easily.
My floppy image hasn't been booting in VMWare for a while. Introduced a second build method, which builds the floppy as an flp image, using ImDisk.
Fixed a small but in the root directory search code which incorrectly left 2 bytes on the stack
I was printing out the 32-byte root directory entry to help me debug, and I was changing the DS register, but not changing it back. Since it was late and night and was tired, this mistake cost me one hour.
Learned about how FAT works in more detail. I can now load the first cluster of the kernel file.
Learned that the data area on FAT12 actually starts with cluster 2. This caused problems with offsetting to locate the first cluster storing the kernel file, before I found this fact online.
Got the loader (load then move to next cluster) code working, adapting an online example. However, it can be tidied up a bit.
Saved 3 bytes by removing a useless mov.
Saved 2 bytes by converting a mov to a dec.
Saved 3 bytes by removing another useless mov.
Saved 2 bytes by removing a push/pop pair and using a different register.
Saved 1 byte by using dec to compare to 0 a register whose values can only be 1 or 0.
I might want better messages and better floppy error handling, which is why I'm tightening up the code.
I've started writing out an example of the cluster chain calculation in comments. Basically, if I'm able to explain it, it means that I've understood it.
I'm trying to understand FAT12 bit packing, and while it's working, I'm uncomfortable that I don't fully understand it. I'm tired now, and will try again tomorrow.
This morning I found the missing link, which is IBM PC's little endianness. When you read the word 0x34 0x12 from memory, you get 0x1234. I've finished writing the example, and everything makes sense now.
I've been cleaning up the boot loader code, with cleaner comments. I also have to remove some debug/dump functions I no longer need. I'll probably add them somewhere else, perhaps a stand-alone file, to serve as examples.
In the mean time, I wrote a quick "hello world" kernel and verified that control is passed to the kernel correctly.
One thing I have not accounted for is floppy disk retries. Real floppy drives (as opposed to those emulated by VMWare and Bochs) often require read operations to be retried. Unless I find a computer with a floppy drive, I will leave it as it is.
Saved 5 bytes by converting segment register assignments to be done via the stack (push->pop instead of mov->mov).
I found that an old USB floppy disk drive I had worked as a boot device, so it's worth trying to get a real bootable floppy.
I downloaded dd for Windows (to copy the floppy image to an actual floppy), and because it doesn't show progress by default, I thought it kept getting stuck, and I kept cancelling the process and re-trying. However, after cancelling, I once tried to see if Windows can read the floppy. Although it had been cancelled half way through, the first sectors, which contain my test files did get copied, and the floppy worked.
After this, I realized dd has a --progress flag, which will show the copy progress in bytes. I created a floppy fully, and verified that the boot loader is loaded.
However, my code currently doesn't retry floppy disk reads, and it needs to, if it is ever to work off of a real floppy. I will get to work on this now.
Fixed a problem where BX was not zeroed, and by luck alone had a value of 0 from before the boot loader was entered.
I got the retry code around every int 13h call, and only have a few bytes to spare. This is because I wanted each int 13h call to display a string so that I could debug any failures which occurred on real hardware.
The loader now has retry code around every floppy operation, and every step is logged via a message to the screen.
Even better, I've tested the boot loader on 2 different floppy drives, on three different computers, and it worked everywhere. This was very good news, as I've been investing a lot of time in making sure the floppy image is as "proper" as possible.
I really wanted the boot loader to initialize all segments for the kernel, but I had to remove that code since I need all the bytes I can get, to have better messages.
Began working on the kernel. The loader only needs some clean up and comments, but is otherwise done.
The kernel registers its first interrupt, 80h, which prints a string. There will be more, dealing with the disk, most likely. They will be made available to apps.
I'm bringing some floppy drive functionality into the kernel, ultimately exposing it via interrupts. I had a choice between this and working on the user console (command line interface). I chose the floppy routines because they're still fresh in my head from the boot loader.
The kernel now exposes interrupt 81h, which loads a file from the floppy disk. I still have to decide where to transform a file name to the uppercase, blank space-padded version, as is stored in the root directory of the floppy. Currently it just loads a hardcoded file name.
More work around making the floppy drive code more reusable, by passing in a pointer to the destination where the file data will be loaded. Int 81h (load file) now returns the success status of the operation.
Decided to reduce the size of the kernel by having the console be a separate app, loaded on startup by the kernel.
Created a caller-app contract, which specifies values of registers, etc. that the consumer of an app must set before running an app. It also mentions rules the app must follow.
The kernel now loads and runs the "startup app" (whose name is still hardcoded).
Wrote some code around restoring whichever video mode was current when the kernel began executing. The idea is to reuse this code for the console app, so that it restores the video mode if any app exits with a different video mode current.
Cleaned up my build scripts and source directory.
I'm writing the routines needed to convert from a lower case string representation of an app's name to a FAT12 file name.
For a change of scenery, I opened up the aSMtris source code to look for calls to int 21h. Int 21h is the entry point into the system services provided by DOS. Since many hobbyist x86 assembly programs assume DOS is present (either natively, or via Windows's NTVDM), int 21h almost ubiquitously makes it into source code, forum posts, tutorials, etc. aSMtris is in the same bucket, but I want to make it run on Snowdrop, because every operating system needs games. As such, I must essentially implement specific int 21h functionality aSMtris uses.
Here are the TODOs for these:
TODO: implement a "clear keyboard buffer" interrupt
TODO: implement "get total timer ticks" interrupt by hooking int 8h
TODO: implement "get next random number" by relying on the get ticks interrupt above and seeding by current RTC seconds value
TODO: implement "print string at coordinates" interrupt
Implemented the "clear keyboard buffer" interrupt
Changed aSMtris "print string" routine to no longer rely on int 21h (since there is no DOS), and instead, rely on Snowdrop's int 80h, which also prints a string.
Hardcoded aSMtris's file name in the floppy load file routine. The kernel loaded it, and then ran it, but aSMtris filled the screen with characters. Quickly after, I realized int 21h expects strings to end in $, whereas Snowdrop expects them to end in a 0 (NULL) character. After updating all strings, aSMtris starts and shows Game Over immediately. This is because Snowdrop doesn't yet implement the routines needed for timing and random numbers.
It felt very satisfying to boot Snowdrop and run aSMtris on real hardware.
Finally removed the hardcoded file name inside the "load file" floppy disk routine
There's a bug in the floppy load file routine... When I try to load the config file, it loops infinitely, and this is not good.
The bug took one painful hour to fix. Not only was I overwriting the disk buffer, butchering the stored FAT, but I also wasn't popping all values on the stack when returning successfully from the config file parser.
Fixed an off-by-one bug which prevented 8-character startup app names from being considered valid.
Startup app names can now be specified using either lowercase or uppercase characters in the kernel config file.
Cleaned up all error messages.
Tested the config file reader code thoroughly.
Added to the app contract a clause on app exit. Upon startup, the caller specifies whether the app is allowed to exit or not. The reason for this is that in some instances, it is undesirable for an app to exit. One such example is the startup app (whatever that may be), which should never exit.
Updated aSMtris to adhere to the "allowed to exit" app contract clause.
Added an interrupt handler for interrupt 1C, which is invoked automatically by interrupt 8 (system timer interrupt).
Noticed that in Bochs, the system timer interrupt occurs significantly more often than 18.2 times a second. I fixed this by configuring Bochs's system timer to be bound to the host machine's system timer.
Rewrote the aSMtris delay routine to use Snowdrop's int 84h. aSMtris now runs as intended on Snowdrop OS, both on virtual machines and real hardware.
Started working on the console app, beginning with correctly handling "special" keys, such as backspace, enter, etc.
The console app now enforces app file name length limit, and is able to load files to memory.
If told to load the console app, it will do nothing.
Backspace and enter keys are now properly handled.
The console app can now load and run other apps.
TODO: better error messages for when apps can't be loaded.
Greatly improved console error messages.
Arrange source files in directories, so that the src tree is cleaner now.
Rewrote the spin wait delay routine in aSMtris into a much simpler version which has no irregularities (unlike the previous version), which ended up as a "delay" interrupt that the kernel exposes.
Introduced a "get random number" interrupt. Ultimately, Snowdrop will rely on both system timer and real time clock to generate random numbers, so that apps don't have to maintain and calculate their own random numbers.
TODO: use values read from the real time clock at boot time to seed the random number generator
Updated aSMtris to no longer calculate its own random numbers, and, instead, rely on the OS-provided interrupt.
TODO: adjust the random number formula to not alternate between even and odd numbers
TODO: manually test aSMtris for random number fairness
Wrote a test program to help me analyze even/odd distribution across a few tens of thousands of random numbers. There is some variation, but nothing too bad.
Started writing the random number initialization routine, which uses the CMOS RTC's current time.
Manually tested aSMtris for random number fairness.
The kernel now initializes the random number generator at boot time based on the RTC (real time clock). When used as a startup app, aSMtris now starts with different blocks every time, as it should.
Went through all source files and added a header which describes the purpose of the file, along with any contracts that the file must respect.
Went through all source files, trimming lines down to 80 characters and removing commented-out code.
Tested most functionality to ensure things still work after the cleanup.
Removed unnecessary files, including the FLP stack, which I haven't used in a while. It seems like the IMG stack produces the exact same results, but has the advantage that it requires no installed Windows programs. This means that I can distribute all tools needed to build the Snowdrop OS floppy image in a single zip which requires no installation.
I just realized that nasm (the assembler I use) optimizes by default. I prefer that the assembler generate exactly what I tell it to, so I turned on the switch -O0 which tells nasm to perform no optimizations. Immediately after doing so, the boot loader exploded in size to 523 bytes. That's 11 bytes I now have to trim.
Since the boot loader had generous amounts of space dedicated to user messages, I was able to shorten some of the messages to gain the 11 bytes back. I was glad I could avoid any further code changes to the boot loader, to keep things stable.
My source code is now assembled without any kind of modifications, such as optimizations.
Cleaned up the build scripts, deleting a bunch of REM'd out trials from back when I was still trying to get the floppy image bootable, as well as accessible via DOS and Windows.
Deleted about 60% of the tools I had downloaded, also from back when I was trying to properly build the floppy image. These tools were no longer used, and I only kept what I needed.
Implemented a kernel interrupt which loads the root directory into the specified memory location. This will be used by the APPS app to print the list of available apps.
Fixed a problem with the build script which erroneously copied the boot loader file to the floppy image.
Hooked up the "dump X bytes" routine to be accessible via an interrupt.
The apps app is now almost complete, and only needs some aesthetic changes.
Finished the apps app.
Wrote a mandatory hello app, which displays "Hello, world!". The hello app is meant to show a very basic example of a Snowdrop OS app which respects the app contract.
Made a few comments changes across the apps.
Started researching tools to convert the floppy image to an ISO CD-ROM image, so that I can also make a bootable Snowdrop CD-ROM as well.
mkisofs seems to be the most popular way of creating CD-ROM images. Unfortunately, mkisofs is a Linux tool, so I will try to find a Windows port.
Found a Cygwin build of the cdrtools suite, which includes mkisofs. Fortunately for me, this build included cygwin1.dll, which removes the requirement that Cygwin be properly installed. I was happy about this because it means that my build scripts continue to require no installed applications in Windows; everything needed to assemble and to create floppy and CD-ROM images of Snowdrop OS are encapsulated in the single Snowdrop OS download.
I've tested that I can boot a VMWare VM using the CD-ROM image I can now generate.
Burned the current Snowdrop CD-ROM image to physical media, and successfully booted it on real hardware.
Found a bug where when the kernel config file is not there, the kernel treats it as if it were.
It turned out to not be a bug; I had forgotten entirely to handle the "kernel config file not found" case. It was easy to fix.
Wrote an app which shuts down host computers which support APM (Advanced Power Management). It didn't take very long to get it working and I've always found it cool how software can shut down hardware.
Tested the shutdown app on real hardware.
TODO: in the console app, change tab characters to print out blanks.
The console app now prints blanks for tabs.
Began cleaning up the directory structure. The point here is to reduce the number of available build scripts, and to hide away the run scripts (those which I used for testing) in their own directory. People who download this will likely be more interested in building Snowdrop than seeing how I ran it, on my particular development environment. However, I am still keeping them for my own sake.
Added a scary "make sure you know what you're doing" message to the "write image to real floppy" script, since it can destroy media if the target drive is not the intended target floppy drive for Snowdrop.
Cleaned up the directory structure even more.
Added a kernel interrupt which offers internal speaker functionality.
Wrote a test app which plays Pachelbel's Canon in D.
Started creating a diagram showing both components and flow of the Snowdrop OS boot and initialization process.
Improved how cluster loading progress is shown by the boot loader.
Finished the sequence/flow diagram for Snowdrop OS.
The Canon in D app now prints to the screen the note being played.
Wrote the bear app which displays a bear.
Made further refinements to the build scripts, in preparation for release.
Moved most of the build script smarts inside the build directory, leaving only two scripts at the project root level: "build all" and cleanup scripts.
Started writing the documentation for Snowdrop OS, contained in the "docs" directory.
Documented all kernel-provided services (via interrupt handlers).
Documented the build process, along with potential issues.
Documented the basic steps to create apps for Snowdrop OS, including setting them as startup apps to run automatically after booting
Documented the recommended steps to extend the Snowdrop kernel.
Wrote an introductory readme.
Performed an online virus scan of the Snowdrop distributable package.
Generated the distributable floppy and CD-ROM disk images and tested them on two virtual machines, and on several real hardware computers.
Tested that the Snowdrop package works out of the box on a fresh installation of Windows XP 32bit.
Snowdrop OS is now ready for distribution!
(the first version of Snowdrop OS was released here)
Started writing the PS/2 mouse driver. I'm going off of various sources of PS/2 mouse documentation and guides.
I got the "reset" workflow to complete, but I'm still missing many pieces.
The difficulty here arises from the wildly different behaviours between PS/2 implementations of virtual machines, real hardware, etc. I'm getting different results all over the place.
Struggled for a whole day with the "get PS/2 status byte and then turn on IRQ12" step. For some reason, printing the PS/2 status byte - right after I asked for it - causes it to no longer be available to read. Since I had plenty of debug printing, this was a pickle to find.
Completed the initialization workflow, and am now looking at creating an IRQ handler.
I was happy to see that IRQ handlers for IRQ12 are registered the same way as all interrupt handlers Snowdrop OS already registers. The vector number for IRQ12 is 74h.
New problem: my IRQ12 handler seems to freeze after the first invocation, even with the proper EOI (End Of Interrupt) being acknowledged to the PIC. It turned out to be that I was not actually reading the PS/2 data port, causing no further IRQs to happen. Tested that the CPU wasn't actually halted by printing something from my timer interrupt handler.
Another difference: real hardware sends a lot more IRQ12s than Bochs. I'll have to see if differentiating by bit 5 of the status byte is enough.
On two virtual machines and two real machines, I get four different results. I am no longer confident that the initialization code will function properly on a variety of computers.
Mitigation strategy: allow user a few seconds during boot to disable the mouse driver. This is to ensure that Snowdrop OS remains functional despite driver-hardware incompatibilities.
Created the mouse interrupt contract. User programs who wish to use the mouse will register a handler which is called every time the driver accumulates 3 "state changed" bytes from the PS/2 mouse.
Wrote a routine which waits for user input (with a timeout), and doesn't load the mouse driver if the user wishes so.
Started on the idea of a "raw" stack, and a "managed" stack.
The raw stack is more complicated to work with, but gives a user program access to raw PS/2 mouse data.
The managed stack is simpler to use, implements a bounding box algorithm, and exposes less data to the consumer.
I'm trying to make it work in more than just 2 test computers. I'm adding timeouts, as that seems to be an accepted practice online. Initial results are not promising.
Now it works well in the 2 VMs, but not on real hardware (on one keyboard doesn't work, and on the other, mouse events are out of sync).
Still working well in 2 VMs, and only intermittently on a 1996 computer that has real, hardware-based PS/2.
It now works reliably on the 1996 computer! A longer timeout during mouse RESET command gave the mouse hardware more than enough time to reset itself.
To my very pleasant surprise, the above fix made it also work on a 2008 computer that has emulated PS/2 (via SMM).
These are great successes; my driver now works on 2 VMs as well as 2 real computers!
It continues to not work on a 2014 computer. I'm not very concerned, since with newer hardware I am at the mercy of their legacy PS/2 implementation. From what I've read, some such implementations are buggy.
Wrote a test program which exemplifies how to poll raw mouse data from a user program.
Wrote a test program which shows how to register a custom interrupt handler, in order to be notified when mouse events take place, and be given the raw mouse event data.
I'm learning about FPU (floating point) x87 opcodes, and how they work. This is the first time I've seen them, and they may be useful for the "managed" mouse mode, to perform divisions when converting to the user-specified coordinate system.
One important fact is that regardless of the coordinate system, a delta X or Y of 1 must always move the mouse position by 1, to allow for the most granular movements in any coordinate system. Higher delta values can scale according to the coordinate system.
Split out the mouse driver source code into two files: communication and higher-level callbacks and polling.
Wrote the mouse manager initialization interrupt.
Created the "poll mouse manager" contract, and wired up interrupts.
Implemented buttons state tracking in the mouse manager.
Added a test program which polls the mouse manager (button state and current location), and displays them on the screen.
Started on the code which translates mouse deltas to locations in user coordinates.
FPU opcodes and operating mode are different than those of the "regular" x86. The x87 FPU operates on a register stack, which I find more confusing than the more traditional "independent" registers paradigm.
I'm making headway into operations such as division and multiplication, needed for the translation to user coordinates.
Translation to user coordinates code is completed. I now have linear mouse movement tracking within a specified bounding box.
Fixed a bug where button clicks registered as +1 movements to the right and downwards.
TODO: Write a graphical example app, to test whether the mouse "feels" good, and adding acceleration if it doesn't.
I'm working on a "mouse GUI" app, which shows how to display a mouse pointer.
Improved the messages displayed by the mouse test apps.
Still working on the mouse GUI app.
More work on the mouse UI app. It's taking a bit longer because I am also writing new routines which draw bitmaps on the screen (for the mouse cursor).
I now have all the bitmap buffer manipulation routines I need to draw the mouse without "erasing" the background behind it.
The mouse pointer now moves on the screen. There is a wrapping issue I have to fix, whereby the mouse appears on the other side of the screen, when it's at the right-most edge.
The mouse is too fast. I have to slow it down.
Simplified mouse-to-box factor calculation by making cursor equally fast vertically and horizontally. This also made it "feel" better.
The mouse cursor no longer "wraps around" the screen when near the right-hand or bottom edges. The mouse GUI test program is now complete.
Introduced acceleration of the mouse cursor when the delta X or Y from the mouse hardware are above a certain threshold.
I will have to adjust the threshold and acceleration amount until they feel right.
Found a sweet spot where I'm happy with how the mouse feels.
Drew a diagram showing the components and interactions of the PS/2 mouse driver.
Documented and cleaned up mouse driver code.
Documented the new interrupts.
(the second version of Snowdrop OS was released here)
Spent some time thinking about how the scheduler would work, and what compromises are needed.
Tightened up kernel code to free up two 64kb memory segments for the memory manager.
Started writing the memory manager. So far it can allocate entire 64kb memory segments.
Wrote a memory allocation test app.
Started writing the scheduler.
The scheduler can now accept tasks, and start them. It still needs task interrupt/resume functionality.
Hooked up the code that starts the startup app to rely on the scheduler and memory manager rather than hardcode things.
The scheduler now supports task yielding. I don't yet have a good way to test it; I will once I spin off console2, which will start apps using the new memory manager and scheduler functionality, rather than just "calling far" into them, and expecting a "return far".
The only way I can test it right now is using one task (the console), which keeps yielding to itself.
TODO: Give each task its own stack.
Spun off console 2 app from the original console. It's still missing at least some error checking.
Started writing a small app to exemplify multi-tasking.
Minor cleanup around the code I recently added.
The two console apps behave differently with respect to programs returning. One calls into the program it loaded, expecting it to return upon completion (old app contract), the other registers the program as a task with the scheduler, then yielding to it. The scheduler later returns to the console either when a loaded app yields to it, or when all loaded apps have exited.
I'll probably upgrade all apps to respect the new contract, and then discontinue the first version of the console.
I'm also considering removing the "don't terminate" part of the app contract, as it isn't technically interesting. In the rare case that somebody may want an app other than the console to run and not terminate, they can provide a non-terminating version to run on startup.
Implemented "task exit" functionality, which allows a program (task) to notify the scheduler that it has finished.
Fixed a bug in console2 which caused a crash when memory could not be allocated for a new task.
Changed the scheduler to allocate each task its own stack.
Fixed a bug in the console which did not free memory when the app failed to load.
In other news, my mouse driver demonstration app no longer works for some reason...
Had a hard drive failure and lost some work.
Investigating why the mouse driver stopped working took me about six gruelling hours. It uncovered an issue with how the flags register was initialized when adding a new task to the scheduler.
Started working on a virtual display manager, so that each task receives its own virtual display.
Basically, once multi-tasking is implemented, most resources have to be virtualized. I will begin with the display, as an example.
Continued work on the virtual display manager.
Continued work on the virtual display manager.
Virtual displays are now initialized and can be recalled into video ram.
TO DO: inspect all places in kernel code where string operations (rep stosb, etc.) are used, and explicitly cld (with pushf/popf) right before.
Updated all kernel procedures which relied on string operations (rep, repe, etc.) to cld right before doing so, and to save/restore flags. This is so changes to the direction flag from user programs do not affect the operation of the kernel.
Started work on the "print string to virtual display" routine.
The "print string" routine is not trivial; it has to update the cursor, as well as write to video ram if the target virtual display is active.
This requires the introduction of the idea of an "active" virtual display, along with a way to "activate" a task's virtual display.
The "print string" routine has a bug in the scrolling code. Once scrolling occurs, no further characters are printed, for some reason...
Fixed the scrolling bug above.
TO DO: add support for the special characters 13 (carriage return) and 10 (line feed).
Added support for CR and LF in the "print string" routine.
Wrote a "move hardware cursor" routine which writes to the 6845 CRT controller to restore the position of the cursor when the active virtual display changes.
More work on wiring up the scheduler and virtual display manager into user-addressable interrupts.
Fixed some bugs which made everything crash upon trying to switch the active virtual display.
Wrote a "print character to virtual display" routine which relies on the existing "print string to virtual display" routine.
TO DO: write a "move cursor in virtual display" routine
More work on the virtual display manager.
Writing to an active virtual display will now write exclusively to the video ram. This is to cover the case of child tasks writing directly to video ram via BIOS, such as in the case of console-started apps that don't require their own virtual display.
With a few hard-coded interrupt calls, the console now starts an app and switches to its virtual display. The app writes a few strings and then exits, re-activating the console's virtual display. This is promising and I'm getting closer to my goal of having multi-tasking and virtual display support.
Wrote a "get hardware cursor position", needed for direct-to-vram string output.
TO DO: decide what happens when the task whose display is active exits (maybe store a task's parent task?)
Fixed an issue whereby the hardware cursor position was not saved when changing the active virtual display.
Migrated the console2 app to use strictly virtual display interrupts to output text, and away from BIOS "write text" interrupts.
Fixed a bug where a non-preserved register would cause a mysterious "0" to appear when the hardware screen scrolled.
While still using some hard-coded calls, the console2 loads and switches to the display of a test app, and then back to itself.
Added (rudimentary, but good enough for now) support for printing out the backspace character. As opposed to the BIOS version which just moves the cursor, my version also erases the previous character.
TO DO: consider moving an app's stack into the first 100 bytes or so (needs all apps to be ORGed differently)
Each task now stores a reference to its parent, that is, the task that started it. This lets the kernel switch to the display of the parent task of a task that's exiting, if the display of the exiting task is active.
New bug, as I'm transitioning out of debug hard-coded calls into changing out of the exiting task's display during a task exit call: cursor is not set properly, and I get some artifacts on the screen.
Spent on hour fixing the above bug. As with most bugs in assembly, it had to do with a register holding a different value than planned.
The only hard-coded debug calls now are the ones that set the active display on app load. The rest is all wired up.
Added logic to store IDs of tasks started by the console app.
Added logic to allow the user to switch between running tasks' virtual displays via ALT+F1 through ALT+F10.
Tested that virtual displays are switched properly upon task exit. Also verified that the user cannot switch to a task's virtual display once the task has exited.
Added a message informing the user of how to switch to the virtual display of a newly started task.
Factored out display driver functionality into 3 source files.
Deprecated the first version of the console app.
Deprecated the memory allocation test app.
Deprecated the first version of the multi-tasking test app.
Updated all apps to use the new "task exit" interrupt when their execution finishes.
Updated all apps to no longer rely on the now deprecated "should return" contract.
Updated all apps which change the video mode to also restore it to its initial value upon exiting.
Found a bug in how console2 tracks completed tasks. Reproduction: run the hello app a few times in a row.
Found a bug in the display driver. Reproduction: run the hello app a few times in a row.
Fixed the bug which prevented the re-use of task list slots in the console2 app.
A bunch of my apps don't output text properly because some of my older interrupts relied on BIOS. I'm converting these interrupts to use my display driver instead, for feature parity with my virtual display routines.
Also, my display driver works only in text mode, so I must change apps which use mode 13h to print text through BIOS.
TO DO: Change aSMtris and mousegui apps to use BIOS for text output in a graphics video mode.
TO DO: Change the apps app to no longer use next segment as data buffer. (either use an in-segment buffer, or allocate memory via the kernel).
Updated the aSMtris and mousegui apps to use BIOS for text output in a graphics video mode.
Updated the apps app to no longer allocate next segment as data buffer. FAT12 root directories have a fixed size, and will fit in the same segment as the app's code segment.
Added error checking in the console2 app when the user tries to switch to the display of a task that exited.
Cleaned up the multitasking test app.
After having observed bunch of people automatically reach for the arrow keys when aSMtris starts in front of them, I updated aSMtris to use left and right arrow keys to move the falling pieces.
Changed any remaining 0x syntax numbers to 0h syntax, for consistency.
Moved routines from kernel.asm into the kernel component source files which needed them. This eliminated two-way dependencies between source files.
Updated the Snowdrop app contract text in all source files.
Encountered a very frustrating bug whereby the graphical mouse test app stopped working. All other mouse apps worked, but not that one. This started happening after routine refactoring in unrelated areas.
Turns out I was only allocating one byte (instead of two) for the scheduler's initial FLAGS register value (upon starting a task). Two bytes are, of course, needed because FLAGS is a 16-bit register. While that single byte was just before a filename buffer, it overran into the first byte of the buffer, running into a happy value. As soon as I moved it into the scheduler, it overran into a bad value, causing undefined behaviour.
Documented the new interrupts, and updated the existing ones.
Cleaned up some code comments.
Drew a diagram showing the components and interactions of the multi-tasking eco-system.
Wrote some documentation.
Modified mousegui and aSMtris apps to switch to their own virtual display upon startup. This prevents the clobbering of the parent task's virtual display.
Renamed console2 to snowshell.
Began implementing a "drop piece" feature in aSMtris.
Finished the "drop piece" feature in aSMtris.
Also switched all controls to the arrow keys. aSMtris can now be played with only one hand. Space bar can still be used to rotate, allowing for two-handed play, as well.
Improved aSMtris's "last chance movement" code, to make the game feel smoother when a piece lands.
The mouse GUI test app doesn't work on real hardware, to my surprise and disappointment. At a first glance, it has something to do with the interrupts becoming disabled at some point. Sure enough, this app works in two virtual machines.
The problem was due to my test machine starting out with the interrupt flag disabled. That value was causing the scheduler to start apps with the interrupt flag off.
Packaged the new version of Snowdrop OS, and re-tested on real hardware.
Re-visited the APM power off code, in hopes of getting it to work on machines other than Bochs.
The power off code now works in Bochs, VMWare, and an old laptop (late 90s). Unfortunately, newer computers don't seem to have APM any longer, causing the code to fail.
Improved aSMtris to allow "last chance" movement after a drop.
Factored APM power off functionality into the kernel, offering it to consumers via a kernel interrupt. Updated the shutdown app.
Found a tool that can create bootable USB keys from my floppy image. I'm still experimenting with it, and may include it in the download pack.
Reverted aSMtris "last chance" after a drop, because I found it to cause me to make more mistakes.
Added to the tool set a tool which creates bootable USB keys from the Snowdrop OS floppy image.
Found a bug in aSMtris which I'm fixing before releasing: pieces which spawn right on top of a tall stack are not rendered, and the "last chance movement" counter takes longer than it should.
Fixed the aSMtris bug. The counter which allowed pieces to be slid was not initialized properly when the piece spawned right on top of a tall stack.
(the third version of Snowdrop OS was released here)
I want add delete/write capabilities to the FAT12 code, and shape it into a nicer file system driver.
Started on FAT12 "delete file" functionality.
Small cleanups to a run script.
Wrote the "mark root directory entry as deleted" part. I'm happy to see that booting a DOS image with the Snowdrop as the second floppy does not show the deleted file via "dir". Similarly, the APPS app no longer shows the deleted app.
Similarly, mounting the floppy image in Windows XP also no longer shows the deleted file, confirming the method.
Spent some time refactoring floppy code. The complexity and duplication was getting a bit out of hand, so it was time it got cleaned up and modularized into proper, reusable routines.
Completed the "delete file" workflow. It still needs some testing, but it looks like it's working for empty files, single-cluster files, and multi-cluster files.
More refactoring in the floppy code. It's time consuming and requires concentration, but it's satisfying when the resulting code is easier to read.
I finally broke down the large and scary "load file" routine into reusable parts, reducing its size to less than half of what is was.
Improved both "delete file" and "load file" to account for how zero-sized files store their first cluster number in the root directory entry.
I'm now writing routines meant to support the "write file" workflow.
Started with a simple "count free clusters", and was happy to see that the in-use and free cluster counts added up to the right total amount.
I think the best way to test either delete or write workflows will be when both are available.
I'm continuing on with "get free root directory entry" and "count free root directories".
Finished the "get free root directory entry" routine.
Finished the "count free root directories" routine.
Added better error messages for fatal floppy failures.
I now have all the parts I need to write the "write file" workflow.
Since the "write file" workflow is complex, I've started by writing out pseudo-code.
"load file" workflow now also returns the file size in bytes.
I've begun implementing the "write file" workflow.
After about two thirds in, I have a bug whereby every other free cluster is skipped, for some reason.
I fixed the bug. I was using a dummy value that exceeded 12 bits when building the cluster chain. This corrupted nearby cluster values in FAT, causing the cluster-skipping behaviour.
The "write file" workflow is now completed, but what I get is a crash, and no file yet.
The issue was an uninitialized pointer. Test file is now created. I can read its contents via DOS, but not via Windows XP, for some reason.
Another symptom of this problem is that Snowdrop's delete code dies trying to delete this file.
If I write a zero-sized file, the problem does not occur.
Snowdrop "delete file" code reads an unexpected value from FAT from the first cluster of a written file. The bug must be with how I update the FAT.
I've isolated the root cause of the FAT write bug to only odd FAT cluster entries. This is promising.
Fixed the bug. I had intended to shift left by a nibble (0xABCD to 0xBCD0), and shifted by one bit, instead of 4.
Windows XP now shows the written file properly, without any complaints.
A lot more testing is needed, including edge cases of full floppies, large files, etc.
Factored out the large FAT12 driver file into four.
Hooked up disk service interrupts and began writing a test app to exercise the disk routines.
Fixed a bug caused by a missed segment assignment.
Ran some basic tests of file creation on real hardware, via both USB and real floppy disk.
Wrote two test apps which fill the floppy data area (with large files), and which fill the root directory with many small files, respectively.
Wrapped vram and virtual "set cursor" functionality and exposed it via a new interrupt.
Implemented a "write attributes to screen" interrupt.
Updated the "get task status" interrupt to also return whether the task's display is currently active.
This interrupt can now be used by tasks that wish to take control, and not yield again, but only when their display is made active.
Implemented a "clear screen" interrupt.
Started work on a file manager, which can be used to browse and manage files.
Contributed further to the file manager, mainly around the UI.
More work on the file manager, focusing on paging.
More work on the file manager paging. Rudimentary page back/forward works now.
Added code to handle the "disk empty" case.
Re-structured code to allow re-initialization, effectively allowing user to work with multiple disks by swapping and re-initializing.
Fixed a multiple month-old bug which manifested itself if keys were pressed during kernel initialization.
The bug had no effect on some machines, froze the keyboard on others, and froze the mouse on others.
Tested the fix on virtual machines and real hardware.
The file manager can now delete the highlighted file, after waiting for user confirmation.
Initialization code is probably lacking something, as deletion from pages 2 and on bugs out the state of the program after re-initialization.
I was incorrectly restoring dirty register values after a re-initialization. The problem has been fixed.
Factored out box and box title drawing routines.
Continued work on the file manager UI.
More work on the file manager UI.
Implemented an unsigned 32bit "integer to string" routine.
The file manager now reports file sizes.
Extended the "integer to string" routine with two formatting options.
Fixed a buffer overrun bug in the "add commas" string format routine.
Fixed a number formatting bug whereby some commas were missing.
File manager now reports the amount of free disk space, relying on a newly-added kernel interrupt.
File manager now reports the amount of available file slots on disk.
Added indicator labels for previous and next pages.
Factored out "read line of text from user" functionality from the shell into an interrupt.
Unfortunately, despite my intentions, the "switch virtual display" workflow of the shell is preventing a bit of code re-use I was aiming for. The shell doesn't just read user input into lines of text; it also has to respond to "change virtual display" key commands.
Added two more formatting options.
Started working on a "page X of Y" label at the top of the file box, in the file manager.
Finished the "page X of Y" label, for the file manager.
The file manager is now complete for the time being.
Started work on the text editor app.
Implemented a string utility which converts 8.3 dotted file names to FAT12 space-padded uppercase format.
Started factoring out useful routines into a set of "common" source files that can be easily included in and re-used by Snowdrop OS apps.
Work continues on the text editor app. User file prompt is now finished.
Added a "dump virtual display to buffer" interrupt.
Added a "dump characters to virtual display" interrupt.
Spent half an hour debugging an issue caused by one wrong letter (used DI register instead of SI).
Wrote the transformation code which runs when the user saves the file.
Fixed a bug in "dump characters" routines which caused erratic behaviour for character counts of zero.
Fixed a bug in "read user input" routines which caused the escape key to be recorded as a key stroke.
TODO: disallow zero-length file names in the text editor.
TODO: write a 8.3 filename validator
Added an interrupt which validates 8.3 filename strings. I was pleased that I wrote it in one go, and the unit tests showed no bugs.
The text editor app now re-prompts user if file name input was invalid.
Factored out some re-usable screen functionality.
Bad news: sometimes, the File Manager app shows an extra, ghost file on the last page. This bug started manifesting itself as the size of the app changed due to the "common" routines changing.
A weird bug has surfaced. I have been testing the file manager with two hundred 10 byte files. Out of all these, one of the files, on page 5 of 14, shows a 3.5Mb size, instead of 10 bytes.
The bug from yesterday is proving to be nasty. Adding a byte before the "root directory" buffer shifts the bytes which represent the erroneous file size forward.
It's hard to debug, because I can't insert debug statements without running the risk of hiding the error.
Skipping some calls before the area where the file size is printed required padding with NOPs, to maintain the same binary size, to keep the error occuring.
This bug has already cost me a few hours, but I have a good lead.
I converted the 3Mb figure into bytes, and it they were 32, 49, 52, 0 (note that x86 is little endian).
I tried ASCII, and noticed that 32, 49, 52 represent the string " 14". During my 10 byte file test, I have 14 pages of files, so this was a hint to the routine which prints "page X out of Y".
Inspected the "page X out of Y" code, and if I change "itoa's" formatting option from 3 to 2, the problem goes away.
Looks like the culprit is the "remove leading zeroes" fragment, invoked when formatting an integer with options 3 and 4 (for interrupt 0A2h).
I have finally fixed the nasty bug from two days ago.
The fix involved changing a single letter. An incorrect data segment meant that the routine clobbered the caller's memory rather than use its own temporary buffer.
Improved the "dump X characters to screen" call to print question marks for non-printable characters such as backspace, line feed, etc.
Replaced aSMtris's ugly hand-drawn arrows with nicer, ASCII ones.
Brought the debug utilities to allow inclusion in apps as well.
Wired together existing "get cursor position" routines, and exposed this functionality via an interrupt.
Started working on cursor movement routines for the text editor.
Work continues on the text editor, on handling special keys, such as home, escape, etc.
More work on the text editor, around character printing.
Fixed a bug in the kernel's "output string routine", whereby a backspace character printed on column 0 would erroneously print a character rather than behave like backspace should.
The text editor can now save.
The text editor is good enough for now, as it does everything I had planned.
The two test apps which fill the disk with files now prompt the user for confirmation at the beginning.
Documented the new kernel service interrupts, and cleaned up source code comments.
(the fourth version of Snowdrop OS was released here)
I would like "format disk" functionality in Snowdrop OS.
Additionally, I want an utility which makes Snowdrop OS bootable disks, similarly to how the DOS SYS command works.
For "format disk", my plan is to use DOS or Windows to format a floppy disk, and then grab the first 33 sectors, saving them as a file that I can then raw-write from within Snowdrop.
For "make bootable", it will be like above, plus the 512-byte loader, plus straight file copying (kernel, configuration file, etc.).
Implemented a "format disk" service interrupt, and a small app to test it.
I don't yet have a proper FAT12 image, so I tested with a hand-created text file of the same length at the FAT12 image.
Used existing tools in my toolchain to make a script which generates a blank floppy disk image, and then extract the first 16.5kb.
The first 16.5kb of an empty 1.44Mb floppy disk image is sufficient to format a new floppy disk by simply raw-writing it starting at logical sector 0 on the disk.
I've always had a strong preference for very simple toolchains. Being able to generate the FAT12 image with no additional tools was very pleasant.
The format app now prompts the user for confirmation.
Wrote a "write boot loader" service interrupt.
Started on the "make boot" app, which makes a floppy disk bootable, and copies core Snowdrop OS files to it.
Refactored FAT12 driver initialization code.
The FAT12 driver now stores the disk's boot sector upon initialization to make OS core file transfer to a new disk easier.
The "make boot" app now copies the Snowdrop OS core files to a new disk, prompting the user to insert the correct disk at every step.
Although I still have to test on real hardware, the "make boot" app behaves well in virtual machines.
For now, I am happy with it, and am moving on to adding support for copying files to the file manager.
Implemented support for reading a file in the file manager.
Fixed an integer-to-string bug whereby after adding commas, the wrong few bytes of memory would be clobbered.
The cause was incorrectly nested push-pop pairs.
Implemented the "write loaded file" workflow.
Adjusted the "empty disk" workflow to take into account that a file may have been loaded from before.
Save must work even when the disk is empty, to support copying a file to another, empty disk.
File manager now reports an error message when it cannot allocate memory.
Created a test app which exercises 32bit integer-to-string formatting.
I decided to make this test app because I've found and fixed more bugs in those routines than I was comfortable with.
I decided I wanted basic support for serial port communications.
Interacting with the serial port is proving to be easy. I can already initialize, as well as transmit (via Bochs, to a file on the host computer) and receive (via HyperTerminal connecting to Bochs).
Arranged all serial port debugging scripts into their own directory inside runscripts\
Wrote a test app which registers a user interrupt handler to receive serial port bytes.
Added a "get serial driver status" system call, just like for the mouse driver.
This is because the user can choose to not load this driver, just like the mouse one.
Added a kernel interrupt that can be used by consumers to register custom interrupt handlers.
Converted kernel initialization and apps to rely on the above call to register their handlers.
The "receive bytes from serial port" app now also sends key presses over the serial port.
Wrote a small test app which sends a message over the serial port.
It looks like seemingly unrelated work has affected the file manager app, which crashes when it loads a file, in preparation for a copy.
The cause was a common library routine which destroyed a register, causing the file manager to clobber the kernel space, likely the FAT12 driver.
Inspected all common library routines for similar problems; found no additional occurrences.
My end-to-end format and "make boot" test includes starting from an unformatted disk, and finishing with a bootable Snowdrop OS disk, which boots into aSMtris.
The initial test has failed. All I get is a disk showing all sorts of 300Gb file entries.
It looks like the formatting utility reads the FAT12 image from the disk to which it is supposed to write it, rather than from the disk current when the operation started.
I'll have to fix that tomorrow.
Split formatting system call into a read-write pair.
After combinations of computers and wires, serial port communications finally worked on real hardware. The problem was either a faulty DB9 gender changer, or an overly sensitive USB-to-serial converter.
As soon as I used a no-frills null-modem cable, I had two 90s laptops talking to each other via the serial port.
Tested, organized, and documented the new scripts.
Documented the new kernel service interrupts.
(the fifth version of Snowdrop OS was released here)
Since Snowdrop OS can now communicate via the serial port, it's time to make a multiplayer game.
However I choose to handle the networking part, I need a way to buffer received bytes.
Implemented a byte-based circular queue to be used to store bytes received via the serial port.
Tested and cleaned up queue routines.
Started work on the multiplayer game. It will be a snake game where each player tries to not collide with the enemy, walls, or their own snake.
Work continues on the snake game. To begin, I want to be able to control the snake using the keyboard.
The "local" snake can now be moved with the arrow keys.
The "remote" snake can now be moved via HyperTerminal.
Began work on the networking routines.
Wrote a service interrupt which gets the ASCII character and attribute byte at a specified row-column location in the virtual display.
Implemented collision detection and transmission of "win" or "lose" packets.
I have been testing using HyperTerminal as the client, and looking at the bytes I receive, while sending directional keystrokes.
Implemented game won and lost workflows.
Implemented the game start sync for server mode.
When both snakes collide at the same time, the winner is now randomly chosen.
Started on the client mode of the game.
This is a crucial part of the development, as it is when the two bridge halves have too meet up and connect.
I've tested all I could without actually copying the executable to a floppy and trying it on my two laptops connected via serial cable.
In client mode, the game now updates snake positions based on data from the server.
In client mode, the game now handles "game over" packets.
Cleaned up code a bit.
Running the game on real hardware shows that the client redraws the snakes only sporadically.
The issue was that bytes were dequeued from the buffer and thrown away if the wrong procedure got to them first.
Fixed it by introducing a "queue peek" procedure and relying on that.
The snakes are now synchronized!
Fixed an issue where the client would render the snakes' heads in the position where they collided, overlapping borders, each other, etc.
Cleaned up colours and messages.
Added obstacles which cause you to lose the game on collision.
Users can now cancel out of "waiting for client/server", exiting the program.
Tested the "peek" queue functionality more thoroughly.
Documented new interrupts.
(the sixth version of Snowdrop OS was released here)
Started adjusting the text editor, to create a new slide show presentation app.
The Presenter app is now ready. It only took me a few hours to write, as I've used the text editor as a starting point.
Created a few sample slides.
Updated the apps app to list apps in multiple columns. This was necessary because the addition of the Presenter app, a single-column display would've scrolled off screen.
The Presenter app now lets the user specify the prefix of the slide file names, to allow loading of multiple presentations from the same disk.
The Presenter app now provides better error information to the user when the first slide cannot be found.
Added an interrupt which converts FAT12-format file names to 8.3 dot file names.
(the seventh version of Snowdrop OS was released here)
Began writing a debug system call which dumps the values of all registers to video ram.
Finished the "dump registers" call. It now also displays up to 4 bytes at the top of the stack, while not crossing segment boundaries.
Added a scheduler interrupt which allows a task to exit, but keep its resources.
It will be useful for programs which provide services to other programs via the interrupts they register.
Decided to change approach from a "exit and keep" interrupt to a "set task lifetime" interrupt, with the task invoking the same interrupt as when exiting normally.
This keeps the app contract unchanged, not increasing its complexity further.
Since this was implemented with an additional task state entry, I was thankful to my past self for leaving a number of unused bytes in each task state entry; it made this change painless.
Implemented a "set task lifetime" system service interrupt.
TODO: NOOP handlers for custom user interrupts
TODO: "get current handler" call
TODO: finish install/call example
Added NOOP custom user interrupt handlers.
Finished install/call example apps.
Improved the example chained interrupt handler to properly maintain all registers when invoking the previous (old) interrupt handler.
This required some meticulous stack operations.
Further improved the chained interrupt handler by reducing its complexity by a third.
Improved chained interrupt handler by reducing its size by another third. This was possible by no longer using the DS register to access memory.
Spent some time re-learning stack frames, since that is relevant material to my recent work involving stack manipulation via BP.
(the eighth version of Snowdrop OS was released here)
I intend to make Snowdrop OS more approachable for game development. As such, I've decided to create a software sprites library that is easy to use by consumers.
Currently, the mousegui app already shows an object (in this case the mouse pointer) that can be moved "over" a background.
I've used it as the starting point, and am now in the process of generalizing some of the app's routines.
Continued work on the sprite library. It now has enough functionality that I transitioned the mousegui app to use the library.
Started adding a simple parallel (LPT) port driver. I'm not yet sure how far I'll take it.
I can now output bytes through the parallel port and see them in a dump file in Bochs. I haven't yet tried it on real hardware.
Factored out graphics mode initialization and restoration logic into a graphics module.
Moved non-sprite specific code into the graphics module.
Renamed and commented all graphics and sprites functions written so far.
Added sprite hide/show routines.
Cleaned up and commented parallel port driver code. Also registered an interrupt handler which sends bytes to the parallel port.
Began writing an app which outputs a byte to the parallel port, while letting the user modify the byte's value.
Adding overlapping sprites revealed some issues with how background rectangles were saved and restored.
In response, I made fundamental changes to how background rectangles are saved.
The sprites of the "sprite demo" app now move in a cycle.
The "parallel port test" app now also allows rotation of the bits of the number being output.
Removed the "wait for port ready" step, since it made real hardware freeze.
The parallel port driver now reports when the computer does not have a parallel port.
I built a small circuit which connects 8 red LEDs to the data pins of the parallel port.
Through the "parallel port test" app, the LEDs can now be turned off or on!
I'd like to be able to load graphics from BMP files, so sprites can be created from them.
I've started reading about the BMP format.
I'm initially going to not support multiple palettes - just the default VGA palette.
I found that nobody has made a 256-colour test BMP which has a correct (matching that on a computer) VGA palette.
I've corrected a test BMP whose palette was almost VGA-proper. It is now suitable as my test BMP file. It is also a good starting point to create further BMPs, as it has the correct VGA palette.
Started working on a test app which displays my test BMP image.
Cleaned up "parallel port test" app.
Fixed reading mismatched height and width from BMP headers.
Experimented with BMPs created with both GIMP and MSPaint.
Wrote a function which reverses the order of all horizontal pixel lines of a loaded BMP file. This is so that they are in the same order as when copied to video memory.
Snowdrop OS can now render a 256 colour BMP file! However, palette loading is not yet supported.
Modified the "sprite test" app to load sprite data from a BMP file.
Introduce a vsync procedure in the graphics module. This is meant synchronize with the monitor's vertical retrace, preventing sprites from flickering.
Began work on loading palettes from BMP files.
I can now load 256-colour BMP palettes, but there's a mismatch between 8-bit BMP palette entries and 6-bit VGA palette entries. This causes images to look very strange at this time.
Converting 8-bit colour to 6-bit colour was as simple as shifting each palette colour right by 2 bits.
I'm having an issue with BMP files because they may or may not contain a so-called "colour space information".
Resolved my previous issue by relying on the "remaining header bytes" header entry, which helps offset correctly to the beginning of the palette data.
Factored out newly-added graphics and BMP routines into their respective modules.
Moved 8-bit to 6-bit conversion to the BMP-specific routine, fixing a bug in the save-then-load palette workflow.
Added a routine which fades the screen to black via palette modification.
Simplified palette loading and saving routines.
Cleaned up kernel initialization order. This was long overdue.
Split out the fade effect routine into a new "graphics effects" module, so apps who don't need effects don't waste memory on the effects module's buffers.
Made the fade effect much smoother by relying on subtraction rather than bit shifts.
Tested loading of BMP images created by various editors (GIMP and MS Paint).
Created BMP test image.
Converted all sprite code to work with 16x16 pixels sprites rather than 8x8. The problem now is that they flicker. It takes too long to render 50 sprites of 256 pixels each. I have to find a solution to this.
I've tested my flickering sprite code on multiple systems, and the results are strange: Old laptop doesn't flicker, new laptop does. Bochs VM doesn't flicker, VMWare does...
I'm starting to think this is related to whether vsync is properly raised.
I'm trying to update all sprite code so that it works with any size square sprites (up to a limit).
I've hit a problem where it seems like I can't use all the memory I'm setting aside for sprites. Something is overflowing somewhere, and it's very difficult to troubleshoot.
Reverted to a "last known good". It will probably be easier to start again than to get out of the rabbit hole I am in.
While making progress again, I identified the issue to be a loop index which wasn't preserved.
Fixed a small issue whereby sprites near the edges of the screen became garbled.
Tested "destroy sprite" functionality.
The sprite library now works with square sprites of any size up to 16x16 pixels. This means that they can moving objects as large as characters and as small as bullets.
TODO: Don't crash when the parallel port is not available
Simplified background generation routine for the "sprite test" app.
Factored out "allocate memory or exit" routine, since it is common to several apps.
While working on making the parallel port driver perform a NOOP when the port is not present, I found that simply entering the "send byte" interrupt freezes computers without the serial port.
The issue was a simple oversight on my part: the handler wasn't installed when no parallel port was present.
Introduced a "get parallel port driver status" interrupt, to keep consistent with other driver status interrupts I've introduced so far. Updated test app to take driver status into account.
TODO: Factor out the high-level workflow of displaying a bitmap on screen, directly from a BMP file.
Clean up and document new features.
(the ninth version of Snowdrop OS was released here)
My plan for the next version is to modify the system timer, so that it is faster than the default 18.2Hz. This will allow for more control over delays used in games and other applications.
Wrote the 8253 PIT interface code, which sets the PIT to a new frequency based on a divisor.
I now have to experiment with a higher frequency, to see how different computers (as old as mid 90s) behave.
Settled on 100Hz as a frequency for the system timer.
Re-organized kernel initialization so that the timer is ACTUALLY the first module which initializes.
Updated all kernel modules references to timer routines, changing them to call timer interrupts. This improves encapsulation.
Simplified scheduler initialization routine.
Verified that no other kernel module calls timer routines directly. Also verified that timer routines do not rely on any interrupts. This ensures that the timer has no co-dependencies with other interrupt handlers.
Added sternly-worded warnings to make future development less likely to break anything timer-related.
Increased the delay value of all kernel references to int 85h.
Updated all references to the previous 55ms timer tick length (it is now 10ms) in kernel comments.
Updated all references to the previous 55ms timer tick length in documentation.
Updated all delay amounts in apps.
(the tenth version of Snowdrop OS was released here)
My next feature is animated sprites.
I have begun modifying the sprites library to allow it to cycle through multiple animation frames for each sprite.
Made rendering faster by caching a pointer to the pixel data of the current animation frame.
Wrote a procedure which sets sprite animation parameters.
Sprites can now animate!
Adapted the "sprite test" app to create an "animated sprite test" app.
Wrote a "clear screen to colour" graphics routine.
Created a nice bouncy ball animation.
Implemented support for sprite animation start/stop.
My next area of focus are sounds.
I already have a routine which can turn on the speaker, but I want apps to be able to generate sounds of various durations without worrying about delays.
This requires a driver which takes in requests for sounds and then manages the playback time of each sound.
For my ZX Spectrum game "Husband Chores", I had created sound playback routines to cope with the computer's lack of a sound chip. They would basically slice a sound over multiple video frames, taking up a fraction of the CPU each video frame, and using it to output sound.
My intention here is to port some of the sound queueing capabilities to Snowdrop OS, so that games can play sounds over multiple animation frames, by queueing them up.
This boils down to porting Z80 assembly to x86 assembly, and then modifying sound hardware-specific parts so they work on IBM PCs.
I ported the sound routines from Z80 to x86. They rely on Snowdrop OS's two sound system calls to start and stop speaker sound output.
I currently have an issue of a previously-enqueued sounds not being thrown out of the queue properly.
Wrote a test app for the new sound routines.
Wrote utilities for displaying 32bit numbers to the screen.
Sound routines test app now displays sound queue information.
Sound playback now supports frequency-shifted sounds, to create nice sweep effects.
Polished the sound test app a bit.
Experimented with moving the "continue playing sounds" routine so that it's invoked by system timer ticks.
It seems to work well, and it would allow the internal speaker driver to be moved into the kernel. The best thing about this is that sounds will now have real time durations, rather than durations relative to the game's frame duration.
It also removes any need for any initialization from user apps.
Moved all sound queueing and playing into a kernel module.
Other than the "normal" (queued) way of playing sounds, I've introduced two more: immediate and exclusive.
A sound added with the "immediate" mode will start playing immediately.
A sound added with the "exclusive" mode will remove all other sounds from queue and keep the queue locked (preventing new sounds from being added) until it finishes playing.
Fixed a bug where a pointer was treated like an offset, causing wacky behaviour for "immediate" mode sounds.
Fixed a bug where sounds which resumed playing after an "immediate" mode sound interrupted them would just be silent.
Documented all "include" directives from the main kernel file.
Cleaned up the sound test app.
TODO: draw diagram of sound driver
Documented new interrupts.
Fixed a bug whereby an empty sound queue would stop the speaker on every system timer tick, rendering the direct-to-speaker interrupts useless.
(the eleventh version of Snowdrop OS was released here)
The last missing piece for Snowdrop OS's game development-oriented capabilities is keyboard handling. The default BIOS keyboard services are not enough to deal with checks such as "is key X currently pressed?", which are vital for video games.
My plan is to create my small keyboard driver, which can be asked about individual key status.
It will likely be based on an interrupt handler sitting in front of the default BIOS handler, chaining to the BIOS handler once it finishes storing key information based on scan codes read from the keyboard controller.
Wrote an interrupt handler which reads the most recent scan code from the keyboard controller.
Read scan code documentation.
Decided to keep things simple, and not care about the extended (escaped) scan codes. This greatly simplifies handler code, with a small cost (not being able to tell left Control key from the right Control key, for example).
Added a list of constants covering the scancodes of the entire 101 key US keyboard.
Started writing a keyboard driver test app which displays the status (pressed/not pressed) of all supported scancodes.
Hooked up a new "get key status" kernel service interrupt.
Completed keyboard driver test app. It has an issue whereby the BIOS keyboard buffer fills, causing the keyboard to become unresponsive and beep.
Clearing the BIOS keyboard buffer on every display loop iteration seems to fix this, but I would like a more elegant solution.
Investigated the keyboard freezing issue further. The problem seems to be a general BIOS approach of locking the keyboard if the buffer is full.
I noticed a similar behaviour in aSMtris as well, so it isn't just related to how my new driver intercepts scan codes.
I'm still deciding how to proceed. I will likely take an approach where a user app which expects multiple, concurrent keystrokes can choose to disable the BIOS keyboard driver before beginning operation. This will keep the buffer empty throughout the app's operation.
The other option is to completely re-write the BIOS keyboard driver - a much larger piece of work, and straying from my goal.
I've chosen to take an approach whereby the keyboard driver can be configured to not call the previous (BIOS usually) handler and issue its own EOI.
I now have a bug whereby skipping BIOS, and then changing the mode to not skip BIOS freezes the keyboard for some reason.
The cause seems to be disabling the BIOS handler invocation while an ALT scan code is still registered as pressed. This is because I'm pressing ALT+F2 to switch to the keyboard test app.
The solution was to modify the "set keyboard driver mode" code to block until all keys are released, before actually changing the mode. This way, the switch between delegating and not delegating to the previous (BIOS) handler is only done when no keys are pressed.
(the twelfth version of Snowdrop OS was released here)