VGA routines from Watcom C
19 Dec 2024Introduction
The VGA era was all about getting the most out of limited hardware. It required clever tricks to push pixels and make things move. To make it easier to work with VGA and related concepts, I put together a library called freak.
This library includes tools for VGA handling, keyboard input, vector and matrix math, and fixed-point math. In this post, I’ll go through each part of the library and explain how it works, with examples.
Be warned - this stuff is old! You’ll need a Watcom Compiler as well as a dos-like environment to be able to run any of your code. I’ve previously written about getting Watcom up and running with DosBox. If you want to get this running you can read the following:
The code that this article outlines is available here.
Video routines
First of all, we’re going to take care of shifting in and out of old mode 13.
Setting a video mode
To shift in and out of video modes we use the int 10h
bios interrupt.
Passing the video mode into al
and setting ah
to 0
allows us to change modes.
We also need to define where we want to draw to. VGA maps to A000:0000
in real mode. Because we’re in protected mode
(thanks to DOS/4G) we set our pointer to 0xA0000
.
Working with buffers
We defined the pointer freak_vga
as a location in memory. From that point in memory for the next 64,000 bytes (we’re
using 320x200x8
which is 64k) are all of the pixels on the screen.
That means we can treat the screen like any old memory buffer. That also means that we can define virtual buffers as long as we have 64k to spare; which we do.
You could imagine doing something like this pretty easily:
We could use memset
and memcpy
to work with these buffers; or we would write our own optimised implementations to
use instructions to move a double at a time (like movsd
and stosd
):
Before flipping a back buffer onto the vga surface, we wait for the vsync to complete. This removes any flicker.
Colours
In mode13, we are given 256 colour slots to where we can control the red, green, and blue component. Whilst the default palette does provide a vast array of different colours; it kinda sucks.
In order to set the r, g, b components of a colour we first need to write the colour index out to port 0x3c8
. We then
write the r, g, and b components sequentially out to 0x3c9
.
To read the current r, g, b values of a colour slot, we write the colour index out to 0x3c7
. Then we can sequentially
read the values from 0x3c9
.
Keyboard
Working with the keyboard is another BIOS service in 16h
that we can call on.
Simple re-implementations of getch
and kbhit
can be produced with a little assembly language:
Fixed point math
The fixed point article that I had previously written walks you through the basic mechanics of the topic. The bit lengths of the whole and fractional parts are pretty small; and unusable. So we’re going to use this technique, but scale it up.
Conversions
First of all, we need to be able to go from the “C type world” (the world of int
and double
, for instance) into the
“fixed point world”. We also need to make our way back:
These macros as just simple helpers to clean up our code when defining numbers.
Constants
Next up, we define some important constants:
Each of these comes in handy for different mathematical operations that we’ll soon walk through.
Trig
We need trigonometry to do fun stuff. To speed up our code, we pre-compute a sine and cosine table that is already in our fixed-point number format:
Our trig tables are based around a nerd number of 1,024
making this a little easier to reason about and giving us an
acceptable level of precision between fractions of radians for what we need.
These are then nicely wrapped up in macros.
Operations
The fixed multiply is a very simple integer-based operation (by design):
Division is also quite similar:
Square roots come in two flavours. A quicker by less precise version (“fast”) or the longer iterative approach.
Finally, some helpers that are usages of existing code that we’ve written are squaring a number, and putting a number
under 1
:
3D
There a primer on Basic 3D that I have done previously that goes into deeper information around 3D mathematics, and more.
Vectors
A 3-space vector has an x, y, and z component.
\[\mathbf{v} = \begin{bmatrix} x \\ y \\ z \end{bmatrix}\]For convenience, we define ths in a union
so that it can be addressed
using the x
, y
, and z
members or as an array through the v
member:
Setting an zero’ing out one of these structures is really just a basic data-movement problem:
Basic arithmetic is achieved using the fixed math primitives defined earlier:
Negate
\[-\mathbf{v} = \begin{bmatrix} -x \\ -y \\ -z \end{bmatrix}\]Addition
Given two 3-vectors:
\[\mathbf{u} = \begin{bmatrix} u_x \\ u_y \\ u_z \end{bmatrix}, \quad \mathbf{v} = \begin{bmatrix} v_x \\ v_y \\ v_z \end{bmatrix}\]Their sum is:
\[\mathbf{u} + \mathbf{v} = \begin{bmatrix} u_x + v_x \\ u_y + v_y \\ u_z + v_z \end{bmatrix}\]Subtraction
Given two 3-vectors:
\[\mathbf{u} = \begin{bmatrix} u_x \\ u_y \\ u_z \end{bmatrix}, \quad \mathbf{v} = \begin{bmatrix} v_x \\ v_y \\ v_z \end{bmatrix}\]Their difference is:
\[\mathbf{u} - \mathbf{v} = \begin{bmatrix} u_x - v_x \\ u_y - v_y \\ u_z - v_z \end{bmatrix}\]Multiplly by Scalar
Multiplying it by a scalar \(c\) results in:
\[c \cdot \mathbf{v} = \begin{bmatrix} c \cdot x \\ c \cdot y \\ c \cdot z \end{bmatrix}\]Divide by Scalar
Dividing it by a scalar \(c\) (where \(c \neq 0\)) results in:
\[\frac{\mathbf{v}}{c} = \begin{bmatrix} \frac{x}{c} \\ \frac{y}{c} \\ \frac{z}{c} \end{bmatrix}\]Length Squared
The length squared (magnitude squared) of the vector is:
\[\|\mathbf{v}\|^2 = x^2 + y^2 + z^2\]Length
The length (magnitude) of a 3-vector is the square root of the length squared:
\[\|\mathbf{v}\| = \sqrt{x^2 + y^2 + z^2}\]Normalise
To normalise a vector (make it unit length), divide each component by its length. Given:
\[\mathbf{v} = \begin{bmatrix} x \\ y \\ z \end{bmatrix}\]The normalised vector is:
\[\hat{\mathbf{v}} = \frac{\mathbf{v}}{\|\mathbf{v}\|} = \begin{bmatrix} \frac{x}{\|\mathbf{v}\|} \\ \frac{y}{\|\mathbf{v}\|} \\ \frac{z}{\|\mathbf{v}\|} \end{bmatrix}, \quad \text{where } \|\mathbf{v}\| \neq 0\]Matricies
A 4x4 matrix is how we store all of our vector transformations. We define it like this:
\[\mathbf{M} = \begin{bmatrix} m_{11} & m_{12} & m_{13} & m_{14} \\ m_{21} & m_{22} & m_{23} & m_{24} \\ m_{31} & m_{32} & m_{33} & m_{34} \\ m_{41} & m_{42} & m_{43} & m_{44} \end{bmatrix}\]Again, we provide both component based access as well as array based access:
Identity
The identity matrix is a special 4x4 matrix where the diagonal elements are 1, and all others are 0:
\[\mathbf{I} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}\]We then simply set a matrix to be an identity matrix, by copying this:
Multiply by Matrix
One of the most important primitives is being able to multiply a matrix by another matrix.
To multiply two 4x4 matrices \(\mathbf{A}\) and \(\mathbf{B}\):
\[\mathbf{C} = \mathbf{A} \cdot \mathbf{B}, \quad c_{ij} = \sum_{k=1}^4 a_{ik} \cdot b_{kj}\]To tidy up the implementation we define a macro mrm
here.
Transforming a vector
Given a 4x4 matrix \(\mathbf{M}\) and a 4D vector \(\mathbf{v} = \begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix}\), the result is:
\[\mathbf{M} \cdot \mathbf{v} = \begin{bmatrix} m_{11}x + m_{12}y + m_{13}z + m_{14}w \\ m_{21}x + m_{22}y + m_{23}z + m_{24}w \\ m_{31}x + m_{32}y + m_{33}z + m_{34}w \\ m_{41}x + m_{42}y + m_{43}z + m_{44}w \end{bmatrix}\]We are ignoring \(w\) by assuming it is 0 in our implementation. We have 3D vectors for simplicity.
Translation
The translation matrix is responsible for moving vectors away from the origin by a given amount.
To translate by \((t_x, t_y, t_z)\), the translation matrix is:
\[\mathbf{T} = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix}\]Scale
The scale matrix will make a point move away from the origin by a given factor.
To scale by \((s_x, s_y, s_z)\), the scaling matrix is:
\[\mathbf{S} = \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}\]Rotation Around an Arbitrary Axis
To rotate around an arbitrary axis \(\mathbf{a} = \begin{bmatrix} a_x \\ a_y \\ a_z \end{bmatrix}\) by an angle \(\theta\), the rotation matrix is defined as:
\[\mathbf{R} = \mathbf{I} \cdot \cos(\theta) + (\mathbf{a} \otimes \mathbf{a}) \cdot (1 - \cos(\theta)) + \mathbf{K} \cdot \sin(\theta)\]Where:
- \(\mathbf{I}\) is the identity matrix.
- \(\mathbf{a} \otimes \mathbf{a}\) is the outer product of the axis vector with itself.
- \(\mathbf{K}\) is the skew-symmetric matrix derived from \(\mathbf{a}\):
Expanding this into the full 4x4 matrix:
\[\mathbf{R} = \begin{bmatrix} \cos(\theta) + a_x^2(1 - \cos(\theta)) & a_x a_y(1 - \cos(\theta)) - a_z \sin(\theta) & a_x a_z(1 - \cos(\theta)) + a_y \sin(\theta) & 0 \\ a_y a_x(1 - \cos(\theta)) + a_z \sin(\theta) & \cos(\theta) + a_y^2(1 - \cos(\theta)) & a_y a_z(1 - \cos(\theta)) - a_x \sin(\theta) & 0 \\ a_z a_x(1 - \cos(\theta)) - a_y \sin(\theta) & a_z a_y(1 - \cos(\theta)) + a_x \sin(\theta) & \cos(\theta) + a_z^2(1 - \cos(\theta)) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}\]Perspective Projection
For a perspective projection with a field of view \(fov\), aspect ratio \(a\), near plane \(n\), and far plane \(f\), the matrix is:
\[\mathbf{P} = \begin{bmatrix} \frac{1}{a \cdot \tan(fov/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan(fov/2)} & 0 & 0 \\ 0 & 0 & \frac{f + n}{n - f} & \frac{2 \cdot f \cdot n}{n - f} \\ 0 & 0 & -1 & 0 \end{bmatrix}\]Conclusion
The freak library is my attempt to distill the essence of classic VGA programming into a modern, accessible toolkit. By combining essential building blocks like graphics handling, input, and math operations, it provides everything you need to recreate the magic of the demoscene or explore retro-style programming.
I hope this article inspires you to dive into the world of low-level programming and experiment with the techniques that defined a generation of creativity. Whether you’re building your first polygon renderer or optimizing an effect with fixed-point math, freak is here to make the journey both rewarding and fun.
Let me know what you think or share what you build—there’s nothing quite like seeing new creations come to life with tools like these!