/*
* HOME(PROJECTS) || RESUME || LINKS || ABOUT
*/

Retrochallenge 2026/10

RTX8080

This year for the retrochallenge, I am doing a purely software project. I am porting the 8080 emulator I originally ported to the TRS-80 Color Computer, to a modern platform. Except, instead of running in on the CPU, I'm going to run it on the GPU, an nVidia RTX3060. The plan is to port the emulator as a bytecode interpreter running in a glsl compute shader. If all goes well, this can instead be used as a pixel shader, and I can render a scene, in which each pixel is calculated by its own instance of an Altair running BASIC. This is a completey absurd and useless project.


This project is also being mirrored on the ACMS Forum https://forum.acms.org.au/t/rc2026-10-rtx8080/1154




Start

October 7, 2024

I wanted to start just by seeing how well some of the code for the existing emulator ports to glsl. I decided to create a small glsl test harness, using some code I had from before. I had previously had a library of C functions, called Zcore, that does a lot of useful things in C: memory management, some basic data structures like lists and vectors, some linear algebra routines for graphics, loading TGA images, etc. It's a bunch of slightly organized functions I've used for random experimentation. The last experiment with Zcore was building it into a full interpreted language called Geranium. It's a FORTH-like stack-based language, that is out-of-scope for this project. But, one of the last experiments in Zcore/Geranium was a rewrite of handling glsl vertex and pixel shaders. I also want to eventually add compute shaders to Geranium, so I decided to just go ahead and make Geranium work again as a library.

I went ahead and added compute shaders to the C helper functions in Geranium, and then started a new project using Geranium as a library. It seems to be working. So, what did the compute shader do? I took some code from the 8080 emulator:


#define INR(reg) \
{                                               \
    ++(reg);                                    \
    S_FLAG = (((reg) & 0x80) != 0);             \
    Z_FLAG = ((reg) == 0);                      \
    H_FLAG = (((reg) & 0x0f) == 0);             \
    P_FLAG = PARITY(reg);                       \
}

from i8080.c

The above is some questionable use of macros, especially for glsl. If you haven't done anything with glsl, it is a C-like language with lots of restrictions, like no recursion and no pointers. The above is a macro that increments a register, which is just a variable. I decided to run it on an array of integers:

void main() {


    int i;
    for (i=0;i<4;i++){

        INR(di[i]);

        di[i] |=di[i]  |  (S_FLAG<<24) | (Z_FLAG<<20) | (H_FLAG << 16) | (P_FLAG << 12) | (0xf<<8);

    }

}

from test.glsl

Now, the above is glsl, but is basically looks like C. It's running on an array of 4 ints, calling INR on each. INR is increment register. This is a set of 4 test cases, di[i] has different possible register values to try thing on. Now what is the next line? I take the value of di[i], and I OR it with the different processor flags, Sign, Zero, etc, in convenient to read positions. I also write an F to indicate that's where the flags are. What were the test cases? That is in the C code:

int idata[4] = { 1, 0xff , 0x7f , 2};
from main.c

When we look at the output:

f02 111f00 1010f80 1f03

Let's analyze each result:
f02: F is the flag 'delimiter', ignore it. 02 is the answer. Looking above idata[0] was 1, so 1 increments to 2, with no flags set.

111f00: idata[1] was 0xff. Increments to the answer of 0. Look at the flags: 111. The leading 0, which is not shown, is the Sign flag. The answer is not negative, so 0 is correct. The next flag is the Zero flag, which is 1 (true), because the answer is zero. Next is the Half carry flag, which I don't yet understand so lets ignore it. Next is Parity. Parity is 1 for an even number of bits, and 0 for odd. There are no bits (count 0), which is an even number, so it is a 1.

1010f80: idata[2] was 0x7f. Increments to 0x80. Flags are 1010: lets check. 0x80 is -1 for a byte, so its negative, and the Sign flag is set. The answer is not zero, so it is not set. I'll skip the H flag, and Parity is 0. Parity is 0 for odd numbers of bits and 0x80 (10000000) has a single bit set.

1f03: idata[3] was 0x02. Well, the answer is 3, its not negative, its not zero, I don't know what the H flag should be, and it's 2 bits, which is even, so Parity is set.

Changes to the code to make this work

Very little had to change. This is the glsl version of the same macro:


#define INR(reg) \
{                                               \
    ++(reg);\
    reg&=0xff; \
    S_FLAG = (((reg) & 0x80) != 0)?1:0;             \
    Z_FLAG = ((reg) == 0)?1:0;                      \
    H_FLAG = (((reg) & 0x0f) == 0)?1:0;             \
    P_FLAG = PARITY(reg);                       \
}

from test.glsl What changed?

Well, that's it for now. Next time I want to do a switch-case and see if I can get an instruction or two to run. I also want to get into explaining how data transfers between the CPU and GPU, because so far I have just shown you some random C code, and told you some of it was actually glsl and magically ran on the GPU. Anyway, the actual compute dispatch happens here, and immediately before that was uploading the buffer of ints (all four of them!) to the graphics card. Following the compute dispatch, is a memory barrier, which waits for the GPU to finish, and then a copy back to the CPU.


October 22

Running a few instructions

I have started porting the i8080-core project to glsl. It's originally a C program, but I have previously modified to run on the TRS-80 Color Computer using a compiler named CMOC, which is extremely similar to C. I would consider it a C compiler that is just missing a few corner cases, that many programs could live without... in this case, an entire emulator! The previous glsl program that ran simple function and returned has been expanded out to run a single 8080 instruction and return. There are two SSBOs allocated for this computer shader: one is a small struct that stores the processor state, and the other is a 64K block of memory. Opcode handler in glsl

The current implementation is extremely ineffecient. The main loop in the C side of the emulator sets up and runs the shader, then does a memory barrier synchronization, reading back the 8080 processor state, prints out some logging, and then repeats. Eventually, the shader will run many instructions at a time, and will only return when a large chunk (1000? 10000?) has completed, or there is some kind of I/O or fault. For now, one instruction at a time is completely appropriate for debugging. Each iteration it prints the program counter, stack pointer, flags, and the contents of the registers. This is printed on to stderr, so it is easy to redirect to a file.

I also took my fork of i8080-core (which supports linux and CoCo) and added identical instrumentation to the linux version.. This allows me to diff the output of the glsl version to the i8080 version... if the registers or program counters diverge, then that's probably where the problem is. I started by implementing a single instrucion... the first instruction of 8080PRE, which is a simple 8080 test program. It was used in the original i8080 repo before I copied it as part of its test suite. I have found that as I implement the instructions one-by-one, they are getting easier. The emulator uses many macros and helper functions, and I've tried to make the translation to glsl one-to-one. In some cases, I just change the definition of the macro, and the actual switch case handler between the two versions are the same. Even a relatively complicated instruction, like jnz (jump if not zero) have identical implementations: glsl vs C. The macros for TST and RD_WORD are a bit different, but since that are building blocks used by other instructions, this gets easier as the macros are figured out.

In the listings below, on the left we have the processor state of running the original i8080-core on a linux computer. On the right, is the processor state of the glsl version. Only a few instructions are implemented, but this serves as a proof of concept. The listings start the same, but diverges. The "fault displayed is a status code coming back from the emulator. I've defined 0x1000 to be unimplemented instruction, with the opcode in the least significant byte. In this case, C5 is not implemented. Looking at the original emulator, this appears to be the push b instruction. Looking at the code for it in i8080-core, it is the same as the already implemented push h instruction, but on a different register. A quick copy/paste, rerun the emulator, and it will advance another instruction before there is a difference. SOmetime it will run a few instructions before they diverge. At least there's a maximum of 256 times I would have to do this! I'm sure glad I didn't choose to emulate something like an x86!

i8080-core 8080.glsl
0100	a:00 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:00000
0102	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:00000
0104	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:11001
0107	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:11001
0109	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:01110
010c	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:01110
0111	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:01110
0117	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:7fe flags:01110
0118	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0119	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
011b	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0121	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0122	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0124	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
012a	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
012d	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:399 flags:01110
012e	a:04 b:00 c:00 d:00 e:00 h:01 l:14 sp:39b flags:00000
012f	a:04 b:08 c:06 d:00 e:00 h:01 l:14 sp:39d flags:00000
0130	a:04 b:08 c:06 d:0c e:0a h:01 l:14 sp:39f flags:00000
0131	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a1 flags:00000
0134	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a9 flags:00000
0135	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a7 flags:00000
0136	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a5 flags:00000
0137	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a3 flags:00000
0138	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a1 flags:00000
013b	a:02 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a1 flags:00000



0100	a:00 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:00000
0102	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:00000
0104	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:11001
0107	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:11001
0109	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:01110
010c	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:01110
0111	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:800 flags:01110
0117	a:01 b:00 c:00 d:00 e:00 h:00 l:00 sp:7fe flags:01110
0118	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0119	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
011b	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0121	a:01 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0122	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
0124	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
012a	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:800 flags:01110
012d	a:14 b:00 c:00 d:00 e:00 h:01 l:14 sp:399 flags:01110
012e	a:04 b:00 c:00 d:00 e:00 h:01 l:14 sp:39b flags:00000
012f	a:04 b:08 c:06 d:00 e:00 h:01 l:14 sp:39d flags:00000
0130	a:04 b:08 c:06 d:0c e:0a h:01 l:14 sp:39f flags:00000
0131	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a1 flags:00000
0134	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a9 flags:00000
0135	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a7 flags:00000
0136	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a5 flags:00000
0137  fault 10c5
0137	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a5 flags:00000
0138  fault 10f5
0138	a:04 b:08 c:06 d:0c e:0a h:10 l:0e sp:3a5 flags:00000
0139  fault 103a

There are some differences in how I have organized the memory and state of the emulator. The original emulator has a byte array. GLSL doesn't actually support bytes... its integer data type is the 32-bit signed integer. There is some limited support for 8 and 16 bit values in later versions or as an extension, but I wanted to keep this project simple. I also don't want to deal with packing and unpacking bytes at every memory operation. Fortunately... the address space of the 8080 is so small, I store memory as an array of 65536 integers. The same is true of registers: A,B, etc are all 32-bit ints. Register pairs like HL are split into two integers! After arithmetic operations that can overflow 8-bits, I mask to 0xFF. This effectively gives me an array of unsigned bytes... which is what I want to work with in an emulator.

For now, I am going to continue working on this, and hope to get 8080PRE running. That app does eventually perform some output using CP/M BDOS calls. Very basic character I/O is implemented in the original i8080-core, and I will adapt that here. BDOS is called by jumping to a specific location. I want to trap that location with a fault: there will be an invalid instruction that the emulator in glsl can't handle, and will return with a fault code. The C host will perform I/O, manipulate the state of the emulator, and then jump back into glsl. Now, I don't want to step on any possible 8-bit opcodes, but because of the way the byte memory is implemented, I'm not actually limited to 8-bit 'opcodes'. The real memory array has 32-bit spaces for each location... so it is completely possible to put an impossible value like 0x8000 there. So, opcode 0x8000 will be like the VMCALL instruction in modern virtual machines.


October 23

First full program runs successfully!

I got 8080PRE to run to completion. I added some extra instrumentation to the linux version of i8080-core, so that it outputs a list of opcodes that were actually executed. I added the same to the glsl version. Diffing them gave a short list of missing opcodes. Most of them were simply copy/paste from the C version. There are some differences, such as the C version using unions to alias bytes and words. This lets H, L, and HL to all be valid. I made a few changes, like POP(HL) is now POP2(H,L), which splits to result into two register variables. POP(PC) is also now POP16, which pops into a single word-sized register. In GLSL, I can't really alias pairs like A and B to also show up as AB, so anything that needs a register pair as a 16-bit value, or needs to store a 16-bit value to a register pair, needs to go thru an extra macro. That's OK! When this is all done, I'm going to diff the glsl and C/CMOC versions, and I suspect I can come up with a version of the switch/case that works on both. For instance, POP2 and POP16 could work in C just fine.

I implemented a function pointer table in C that lets you plug-in whatever syscalls you want. Here is the C_WRITESTR implementation:


/* 

C function pointer syntax is tricky:

if syscalls[256] is an array of 256 entries...

then (*syscalls[256]) means each entry is a pointer...

and then (state8080* state, int* mem) means that pointer to to a function that accepts there two args

*/

(*syscalls[256])(state8080* state, int* mem) = {NULL};


//CP/M call to write a string to the console.  Strings are "$" terminated!  

void C_WRITESTR(state8080* state, int* mem){
	int ptr = (state->D <<8 | state->E) & 0xffff;

	while (mem[ptr]!='$'){
		fputc(mem[ptr++], stdout);
		ptr &= 0xffff;
	}

}


Then when setting up:


	intdata[5] = 0x8000; //trap instruction for CP/M calls
	syscalls[9] = C_WRITESTR;

In glsl, the undefined opcode fault handler catches it:

  switch(op){
	// Actual handlers go here

        default:
            current.fault = 0x10000000 | op;
  }

And in C, it is handled like this:



	if (state.fault & 0x10000000){
		int op = state.fault&0xffff;


		if (op == 0x8000){

			int call = state.C;

			fprintf(stderr, "syscall %x\n", call);

			if (syscalls[call])
				syscalls[call](&state, intdata);

			state.PC = intdata[state.SP];
			state.SP+=2;
		}
	}

To call a C function from the 8080 host, all that needs to happen is you need to write the C function that accepts the CPU state and memory array, and then drop it into the function table.

Next are the rest of the CPU exercisers, and then... Altair BASIC. If Altair BASIC runs (and I would expect it to, since it ran on the TRS-80 version of this emulator), then... we have a GPU implementation of BASIC. What to use it for? I would like to run it in a pixel shader. One 'hiccup' is that the memory array is a SSBO, so to run it for a pixel, each needs their own private space. But, RD_BYTE and WR_BYTE are macros... which means I can replace them with whatever complicated function I want. Things that don't change per pixel, like the BASIC code and the ALTAIR BASIC itself, can be shared. The only thing that needs to be replicated and private are the stack, and any variables, internal state, etc. What I want to do is run a single instance of Altair BASIC up to the point where the program has been 'typed in', RUN is typed, and its just waiting for the return key. This state will then be in the shared SSBO 'ROM.' Then, there will be a small 'page table' for each pixel, and parts modified per-pixel will be stored seperately. If fixed-sized pages are used, then each pixel has a small fixed-sized pre-allocated section of a large shared array to maintain that page table. When a new page is being written, each shader can atomically allocate a page from a shared large array of pages. A similar per-shader dynamic allocation method is done here to allocate sparse structures in glsl shaders: Compacting Draw Indices with Atomic Adds in GLSL.


October 26

Altair BASIC

Altair BASIC is working on the GPU. It doesn't do much yet but run BASIC programs on the terminal.

Next steps: