Como desarrollé un compilador de C para Am29000 y el navegador de Internet
por Oscar Toledo G. 16-ago-2026
Si leíste my artículo previo, ya sabrás que
desarrollé un sistema operativo con ventanas en código máquina de 32 bits para una computadora casera basada en el procesador Am29000. En este artículo, hablaré del desarrollo de mi compilador de C para estos procesadores, y un navegador web.
El periodo de tiempo es navidad de 1998 y hasta mi cumpleaños en 1999. Tenía 20 años. Internet se estaba extendiendo como fuego en México, Bruce Willis justo había salvado la tierra de un asteroide gigante, nuevas carreras emergieron para la naciente Internet (fue un año de oro para los diseñadores gráficos), la gente estaba asustada de que el bug del año 2000 lanzara un apocalipsis digital (incluso los Simpsons pasaron un episodio donde Homero olvida actualizar las computadoras), y Arnold Schwarzenegger estaba matando demonios a balazos en El Fin de los Días.
Encontremos un compilador de C
Por 1997 desarrollé varias utilidades, controladores de impresora (tenía una HP DeskJet 500 y me las arreglé para imprimir en color con una Epson Stylus 600), también me las arreglé para enviar y recibir faxes usando una tarjeta de modem. Era una época en que todos preguntaban si tenías fax para enviarte anuncios, u obtener información. Incluso compramos una máquina de fax, y el siguiente año, nadie volvió a pedir un fax. ¡Bienvenido al e-mail!
De cualquier forma, trabajar en código máquina era complicado, y era como realizar buceo profundo en aguas lodosas. A menos que tuvieras una mascara para ver en el fondo (las notas de direcciones y documentación) te perdías cada vez más y más.
Incluso con toda mi energía adolescente, comencé a sentirme cansado, porque no podía codificar nuevas funciones sin diseñar un plan de memoria cuidadoso, como mover el código para hacer espacio, o peor, relocalizar varios saltos e introducir errores inesperados porque me faltó un cambio. En algún punto, pensé "esto puede crecer" y dentro del código puedes encontrar secuencias de 5 a 10 instrucciones NOP para futura expansión. Otra cosa que se puede encontrar son rutinas fuera de lugar, porque no cabían en el espacio original.
Como ya era un visitante regular de un café Internet (o más conocidos en México como cibercafé). Uno de los primeros cafés de Internet estaba ubicado justo cruzando la calle del desaparecido Bazar Pericoapa, y también servían café. Explorábamos Internet al ritmo de "Ciega, sordomuda", "Amor de papel", “Laura no esta” y “Barbie girl”. Por supuesto, pronto reconocieron su error cuando los capuchinos y expresos se derramaron en los teclados, y el café nunca se volvió a servir.
Estaba buscando cualquier cosa acerca del procesador Am29000, y me enteré del compilador High-C 29k y el compilador GNU C v2.8.1 con soporte para Am29000. Pero no tenía forma de comprar el compilador High-C 29k, así que descargé solamente las fuentes del GCC, y encontré que requería 2 megabytes de RAM en la computadora (y probablemente más si pensamos en la memoria virtual), cuando mi computadora solo tenía 512 kb. de RAM. Peor, requería dos programas extras: Flex y Bison.
También requería mucho apoyo del sistema operativo, que yo vagamente tenía (más un ensamblador y un enlazador). Necesitaba arrancar el compilador de alguna forma, pero me detenía completamente la idea de portar dos programas enormes para un solo uso. Así que recurrí a una galaxia más cercana:
mi compilador de C para transputer.
Mi problema principal era la arquitectura completamente diferente del procesador Am29000 con muchos registros. Y no podía imaginarme como asignar los registros en mi compilador de un solo paso. Era muy importante que las variables normales pudieran caber en registros locales, pero si una sola indirección aparecía (por ejemplo, &a) entonces esa variable debía quedar en memoria.
Mi primer intento fue un port del compilador de Small-C al Am29000, se que lo hice porque puse una nota en mi bitácora de diciembre de 1997. Probablemente fue una tremenda falla y no tenía utilidad, porque no hay ninguna mención más.
De nuevo el 2 de febrero de 1998 menciono que necesito urgentemente un compilador de C e instalé DJGPP (un compilador GCC portado a MS-DOS) en una PC 80486 para ayudar con el desarrollo. No podía usar el transputer porque solo tenía 128 kb. de memoria.
DJGPP es la abreviación de DJ G++, No pueden ni imaginar cuanto ayudó DJ Delorie a los desarrolladores de todo el mundo cuando los compiladores todavía se vendían a precios altos, y esta persona creó una versión del compilador GNU C++ para DOS que funcionaba de inmediato.
Cultivando un compilador en el árbol
Fue hasta el 6 de mayo de 1998 cuando tomé el código fuente de mi compilador de C para transputer, y me las arreglé para compilarlo con DJGPP como una prueba. Esto significa que tuve que reemplazar mis funciones no estándar de entrada y salida con las funciones de la librería C estándar.
Mi bitácora no incluye mayor información, pero mientras buscaba más datos, encontré que preservé los pasos de la creación del compilador de C para Am29000 en un disco flexible. Aquí hay una foto del disco con mi progresión de compiladores de C. Tenía una vaga idea del control de código fuente porque había leído acerca de SCCS (Source Code Control System), y mi forma de hacerlo era "copiar todos los archivos del día en un disco flexible".
Este disco flexible contiene dos compiladores de C mejorados, y la primera versión del compilador de C para Am29000.
Este compilador de C para transputer ahora funcionaba en una máquina PC igual que el transputer original. Los árboles de expresiones estaban preservados en arreglos. Un arreglo para apuntar a los nodos de la izquierda, otro arreglo para apuntar a los nodos de la derecha, otro arreglo para el valor del nodo, y otro arreglo para el tipo de nodo. Por supuesto, esto significa que no puede crear expresiones complejas sin expandir el arreglo como fuera necesario. Puede encontrar este compilador en el
git de mi transputer en el directorio cc0.
Este es un fragmento de código del árbol de expresiones como un arreglo (función crea_nodo):
++ultimo_nodo;
if(ultimo_nodo == TAM_ARBOL) {
error("Expresión muy compleja");
cancela();
}
nodo_izq[ultimo_nodo] = izq;
nodo_der[ultimo_nodo] = der;
oper[ultimo_nodo] = op;
esp[ultimo_nodo] = val;
regs[ultimo_nodo] = 0;
regsf[ultimo_nodo] = 0;
Lentamente diseñé un plan: Había una sola forma de crear un generador de código Am29000. Necesitaba analizar la función completa en memoria, y entonces sabría cuantas variables locales eran requeridas, detectar referencias a variables locales, y podría construir entonces un asignador de registros.
A continuación rediseñé el generador de árboles de expresiones usando memoria dinámica (
malloc/
free), y usando
struct. Todavía hecho para el transputer (
vea el directorio cc1). Por mis notas, en los descansos también jugaba una demo de Tomb Raider 2.
Este es otro fragmento de código mostrando como cambió la creación de nodos:
ultimo_nodo = malloc(sizeof(struct nodo));
if (ultimo_nodo == NULL) {
error("Expresión muy compleja");
cancela();
}
/* ... */
ultimo_nodo->izq = izq;
ultimo_nodo->der = der;
ultimo_nodo->oper = op;
ultimo_nodo->esp = val;
ultimo_nodo->regs = 0;
ultimo_nodo->regsf = 0;
Este código es bastante más legible que el original, y también solo limitado por el total de memoria disponible.
El 13 de mayo de 1998 finalmente tomé el toro por los cuernos, y comencé a trabajar en el análisis principal para convertir el código en una representación intermedia de árboles con listas enlazadas. Una secuencia de sentencias se volvió una lista enlazada, y cualquier sentencia anidada se convirtió en una rama de la lista. Me dio un resfriado esta vez, y vi “El libro de la selva” con Jason Scott Lee en Laserdisc, y después de recuperarme me fui directamente a crear el generador de código para el procesador Am29000.
El traslado completo me tomó dos semanas, y tuve que hacer varias pruebas pequeñas para el generador de código. Por ejemplo, este es el generador de código para el transputer:
/*
** Codigo para cada operador binario, y algunos unarios.
*/
gen_oper(oper, rev)
int oper, rev;
{
if (oper == N_NULO) return;
if (oper == N_CUENTA)
emite_linea("wcnt");
else if (oper == N_OR)
emite_linea("or");
else if (oper == N_XOR)
emite_linea("xor");
else if (oper == N_AND)
emite_linea("and");
else if (oper == N_IGUAL) {
emite_linea("diff");
emite_linea("eqc 0");
} else if (oper == N_SUMA)
emite_linea("bsub");
else if (oper == N_MUL)
emite_linea("prod");
Y este es el mismo fragmento para el procesador Am29000:
/*
** Codigo para cada operador binario, y algunos unarios.
*/
gen_oper(oper, inmediato, reg1, reg2, constreg, control)
int oper, inmediato, reg1, reg2, constreg, control;
{
int reg;
if (oper == N_OR || oper == N_AOR) {
gen_inst1("or", inmediato, reg1, reg2, constreg);
} else if (oper == N_XOR || oper == N_AXOR) {
gen_inst1("xor", inmediato, reg1, reg2, constreg);
} else if (oper == N_AND || oper == N_AAND) {
gen_inst1("and", inmediato, reg1, reg2, constreg);
} else if (oper == N_CD || oper == N_ACD) {
gen_inst1("sra", inmediato, reg1, reg2, constreg);
El transputer con su arquitectura de pila toma cuidado del uso de registros, pero en el Am29000 el compilador controla como se usa cada registro. Y ahora para un ejemplo de la complejidad del procesador, este es el código para iniciar una función en C:
/*
** Prologo de función:
**
** o Asigna las variables virtuales a los registros o a la memoria.
** o Asigna el espacio requerido.
** o Copia los argumentos de la entrada (si es requerido)
*/
prologo_funcion()
{
int variable, temp, por_copiar = 0, posicion, registro;
/*
** Asignamos los registros (por el momento no se sabe si van a ser locales
** o globales), también asignamos espacio en la pila pero aún falta
** determinar si va a ser corrida para hacer espacio a argumentos que
** deben ser copiados.
*/
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 0: /* Variable para asignar como se pueda */
if (virtuales[variable + 1] != 0) { /* ¿ Necesita apuntador ? */
virtuales[variable] = (pila << 2) | 1;
pila += virtuales[variable + 2] ? 8 : 4;
} else { /* No, queda en registro */
if (virtuales[variable + 2]) /* Alinea punto flotante */
pila_regs = (pila_regs + 1) & ~1;
virtuales[variable] = pila_regs << 2;
pila_regs += virtuales[variable + 2] ? 2 : 1;
}
virtuales[variable + 1] = 0;
break;
case 1: /* Variable que debe quedar en memoria */
temp = virtuales[variable] >> 2;
virtuales[variable] = (pila << 2) | 1;
pila += temp;
virtuales[variable + 1] = 0;
break;
case 2: /* Cálcular cuantos argumentos debemos copiar */
if (virtuales[variable + 1] != 0) /* ¿ Necesita copiar ? */
por_copiar += virtuales[variable + 2] ? 8 : 4;
break;
}
variable += 3;
}
/*
** Corremos la pila para hacer espacio a los argumentos que deben copiarse,
** también copiamos los argumentos y pre-asignamos registros a los args.
*/
pila += por_copiar;
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 1: /* Variable que debe quedar en memoria */
virtuales[variable] = (((virtuales[variable] >> 2) +
por_copiar) << 2) | 1;
break;
}
variable += 3;
}
if (pila != 0)
gen_inst1("sub", SI, 125, 125, pila);
pila_regs = (pila_regs + 1) & ~1;
posicion = 0;
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 2: /* Copiamos los argumentos requeridos */
if (virtuales[variable + 1] != 0) {
virtuales[variable + 1] = 0;
registro = virtuales[variable] >> 2;
virtuales[variable] = (posicion << 2) | 1;
if (posicion == 0) {
gen_inst2("store 0,4,", NO, registro + 128, 125);
posicion += 4;
if (virtuales[variable + 2]) {
gen_inst1("add", SI, 96, 125, posicion);
gen_inst2("store 0,4,", NO, registro + 128, 96);
posicion += 4;
}
} else {
gen_inst1("add", SI, 96, 125, posicion);
gen_inst2("store 0,4,", NO, registro + 128, 96);
posicion += 4;
if (virtuales[variable + 2]) {
gen_inst1("add", SI, 96, 96, 4);
gen_inst2("store 0,4,", NO, registro + 129, 96);
posicion += 4;
}
}
} else {
if (total_regs == -1 && pila_regs <= 4)
temp = 128;
else if (total_regs == -1)
temp = 130 + pila_regs;
else
temp = 130 + total_regs + pila_regs;
virtuales[variable] = (((virtuales[variable] >> 2) + temp)
<< 2) | 2;
}
break;
case 3: /* Ajustamos los argumentos que vienen en memoria */
virtuales[variable + 1] = 0;
virtuales[variable] = (((virtuales[variable] >> 2) + pila) << 2) | 1;
break;
}
variable += 3;
}
if (total_regs == -1 && /* Si no se llama ninguna función y solo hay */
pila_regs <= 4) { /* 4 registros utilizados o menos, */
pila_regs = 0; /* No nos hace falta la pila de registros */
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 0: /* Asignar registros gr116 - gr119 */
virtuales[variable] = ((virtuales[variable] >> 2) + 116) << 2;
break;
case 2: /* Los parametros siguen en locales */
virtuales[variable] &= ~3;
break;
}
variable += 3;
}
} else { /* Pedimos espacio en la pila de registros */
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 0: /* Asignar registros locales */
virtuales[variable] = ((virtuales[variable] >> 2) +
total_regs + 130) << 2;
break;
case 2: /* Los parametros ya tienen sus posiciones */
virtuales[variable] &= ~3;
break;
}
variable += 3;
}
pila_regs += total_regs;
pila_regs += 2;
if (pila_regs > 128)
error("Demasiadas variables locales");
else if (pila_regs + pila_args > 508)
error("Demasiados argumentos");
gen_inst1("sub", SI, 1, 1, pila_regs << 2);
emite_linea("asgeu 64,gr1,gr126");
gen_inst1("add", SI, 129, 1, (pila_regs + pila_args) << 2);
}
}
Cada variable local del C, incluyendo argumentos de funciones, se vuelve una variable "virtual" (en mi línea de pensamiento una variable que no se ha asignado a nada todavía, es virtual). El tipo 0 es una variable normal (con una cuenta de indirección para detectar si debe ser copiada a memoria), el tipo 1 es un arreglo, y el tipo 2 es un argumento (de nuevo con una cuenta de indirección).
Hace espacio en la pila de memoria (gr125) si se requiere, y entonces copia cualquier argumento que debe estar en memoria (cuando se pasan structs, o porque el operador & es usado), y después de hacer esto procede a asignar registros locales para las variables restantes. Es bastante avanzada la detección de cero llamadas a función para evitar completamente el marco de pila y usar los registros gr116-gr119 como locales, y finalmente viene la muy simple creación del marco de pila en tres instrucciones (sub, asgeu, y add)
El epílogo de la función a la vez se ve muy simple:
epilogo_funcion()
{
if (buffer_vacio)
return;
if (pila_regs != 0) {
gen_inst1("add", SI, 1, 1, pila_regs << 2);
if (pila == 0)
gen_libre(0);
else
gen_inst1("add", SI, 125, 125, pila);
emite_linea("jmpi lr0");
emite_linea("asleu 65,lr1,gr127");
} else {
if (pila != 0)
gen_inst1("add", SI, 125, 125, pila);
estado_buf[total_lineas] = 10;
emite_linea("jmpi \1\1\1\1\1\1\1lr0");
gen_libre(1);
}
vacia_buffer();
}
La primera versión del código fuente del compilador de C para el Am29000 esta disponible en
mi git en el directorio cc.
Al mismo tiempo que trabajaba en el compilador, también desarrollé el ensamblador para procesar las instrucciones Am29000 en un binario, junto con una pequeña librería para interfazar mi sistema operativo de ventanas.
El ensamblador es muy pequeño y directo porque el conjunto de instrucciones del Am29000 es ortogonal, esto significa que los registros se pueden usar indistintamente en cualquier instrucción, y que hay simetría en las instrucciones (por ejemplo, todas las instrucciones aritmético/lógicas tienen tres operandos). Esta temprana versión MS-DOS del ensamblador también esta en mi git en el directorio asm.
Finalmente, comencé a trasladar el compilador a mi sistema oeprativo. Me tomó bastante tiempo hacer que se compilara a si mismo, porque las fugas de memoria llenaban la pequeña RAM. El mayor bug fue que olvidé liberar la memoria de los árboles de expresiones después de procesar cada función. De cualquier forma también había un monton de errores en el generador de código que requerían correcciones urgentes, y fue hasta el 27 de mayo de 1998 que el compilador se volvió capaz de generar el mismo listado ensamblador que la versión PC.
Para ensamblar la salida del compilador, requería el ensamblador corriendo dentro del sistema operativo, así que imprimí el código fuente del ensamblador que escribí en lenguaje C con la PC, y lo porté a mano a código máquina. Finalmente, el 1° de junio de 1998 logré compilar el compilador de C, ensamblarlo, y generar exactamente el mismo binario cada vez.
No pude encontrar pistas de ese ensamblador en código máquina, pero mientras lo pensaba, me parece recordar que logré compilar la versión en C, y estaba tan contento que simplemente moví el ensamblador al folder correcto para probar el compilar con este, y funcionó, pero unos minutos después me di cuenta que había reescrito mi ensamblador en código máquina.
Tenía un compilador de C, pero no había forma de editar programas, así que comencé a codificar un editor de texto en código máquina el 22 de junio de 1998, y logré que funcionara bien por el 1° de julio. El editor de texto se componía de 50k de código máquina, y se uso como tal por varios años. Hasta ahora eran dos meses completos para crear un entorno de desarrollo completo (editor de texto, compilador de C y ensamblador)
Una vez que el editor de texto estaba listo, pude sacar los errores del compilador uno por uno, como el soporte defectuoso de punto flotante, asignación incorrecta de structs, y código poco eficiente. La prueba final fue compilar el modelador poligonal 3D que hice para mi sistema operativo de transputer, y este fue el clavo final en el ataud del transputer.
Ahora para el sistema operativo con ventanas
Originalmente el compilador de C se escribió para la computadora G11V1, y todo esto fue desarrollado con un disco duro SCSI. No tengo la menor idea de donde puede haber quedado. Solo fue por unos pocos meses, ya que el 18 de junio de 1998 se portó todo a la nueva G11V2.
La diferencia principal entre ambos sistemas era el orden de bytes. G11V1 tenía un orden de bytes big-endian, mientras que la G11V2 usaba el orden little-endian. Esto era relativamente fácil porque el procesador Am29000 tiene un bit de orden de byte que puede ser configurado.
También la G11V2 utilizaba slots ISA y tenía tres slots PCI (de conectores reciclados de motherboards 486). Esto era porque las tarjetas ISA iban de salida, y las nuevas tarjetas de video llegaban como PCI.
Este artículo es posible porque hice siete discos flexibles con los archivos casi completos de mi sistema operativo incluyendo código fuente y programas de apoyo. Tres son del 30 de diciembre de 1998, y cuatro más del 24 de abril de 1999. Es una explosión de información respecto al único disco flexible de primavera de 1997.
Mi conjunto de respaldos en disco flexible de 1998 y 1999.
Sin embargo, estos discos no cubren mi sistema operativo de ventanas porque estaba en ROM. La G11V2 comenzó con 512 KB. de RAM, y una forma de tener más espacio para los programas era mover el sistema operativo justo en el 1 MB. de ROM, liberando 256 KB. de memoria para programas. Así que miré en mis archivos tratando de encontrar la imagen de EPROM de G11V2.
Al fin encontré dos imágenes del sistema operativos de ventanas (simplemente llamado FENIX.BIN). Por alguna razón, nunca actualicé los mensajes de copyright, así que ambas eran muy similares.
Me tomó como dos horas de aburrida comparación binaria hasta que descubrí la tabla de clases de ventanas. Algunas funciones todavía estaban en 0x000f0000 a 0x000fffff mientras que en la otra versión estaban en 0x00030000 a 0x0003ffff. Esto era para hacer espacio para otro programa dentro de la ROM.
Finalmente, encontré la fecha de copyright que buscaba: Decía 1996-1999 en la ROM con funciones en 0x000f0000 to 0x000fffff. Y era 1996-2000 para la otra ROM con funciones en 0x00030000 to 0x0003ffff.
Carguemos esto en el emulador
I needed to put this in the emulator, make sure the little-endian byte order was selected, and try to run it. I expected a few hurdles in the way, because the G11V2 computer uses a PCI video card.
After putting together the ROM file, it booted in a very similar way, again I patched the gr95 register to 0x00040040 to avoid the traps running the math emulation code. The first stop indeed was a LOAD 0,0x00,gr98,gr96 instruction where it used the address 0xc8000000.
This address gets the PCI configuration space for the first slot. If it doesn’t find a card, it tries two more addresses 0xd0000000 and 0xe0000000.
I was surprised to see this code:
0x0004A810: 0x03006400 CONST gr100,0x0000
0x0004A814: 0x02086400 CONSTH gr100,0x0800
0x0004A818: 0x03006000 CONST gr96,0x0000
0x0004A81C: 0x02c06000 CONSTH gr96,0xc000
0x0004A820: 0x92606064 OR gr96,gr96,gr100
0x0004A824: 0x16006260 LOAD 0,0x00,gr98,gr96
0x0004A828: 0x03106300 CONST gr99,0x1000
0x0004A82C: 0x02006301 CONSTH gr99,0x0001
0x0004A830: 0x60636263 CPEQ gr99,gr98,gr99
0x0004A834: 0xac006306 JMPT gr99,0x0004a84c
0x0004A838: 0x70400101 NOP
It tries to find a SYM53C810 SCSI card. I had forgotten completely about it. It isn’t required now, because I can patch out the SCSI controller and reuse my subset of SCSI commands to handle an emulated hard disk drive. In my daily log I was incredibly happy I got the SYM53C810 manual direct from the manufacturer so I could do faster SCSI access.
The following code was this one:
0x0004C23C: 0x03006600 CONST gr102,0x0000
0x0004C240: 0x02086600 CONSTH gr102,0x0800
0x0004C244: 0x03006000 CONST gr96,0x0000
0x0004C248: 0x02c06000 CONSTH gr96,0xc000
0x0004C24C: 0x92606066 OR gr96,gr96,gr102
0x0004C250: 0x16006160 LOAD 0,0x00,gr97,gr96
0x0004C254: 0x03c36280 CONST gr98,0xc380
0x0004C258: 0x02006204 CONSTH gr98,0x0004
0x0004C25C: 0x03006301 CONST gr99,0x0001
0x0004C260: 0x16046462 LOAD 0,0x04,gr100,gr98
0x0004C264: 0x60656461 CPEQ gr101,gr100,gr97
0x0004C268: 0xac006506 JMPT gr101,0x0004c280
0x0004C26C: 0x03006400 CONST gr100,0x0000
0x0004C270: 0xb4ff63fc JMPFDEC gr99,0x0004c260
0x0004C274: 0x15626208 ADD gr98,gr98,0x08
0x0004C278: 0xa0ff00f3 JMP 0x0004c244
0x0004C27C: 0x81666601 SLL gr102,gr102,0x01
0x0004C280: 0x15606204 ADD gr96,gr98,0x04
0x0004C284: 0x16046060 LOAD 0,0x04,gr96,gr96
0x0004C288: 0x70400101 NOP
0x0004C28C: 0xc8008060 CALLI lr0,gr96
0x0004C290: 0x70400101 NOP
It again reads the PCI configuration space, and tries to find one of the supported video controllers:
0x0004C380: 0x00b81013 ???
0x0004C384: 0x00060000 ???
0x0004C388: 0x96601023 XNOR gr96,gr16,gr35
0x0004C38C: 0x00062000 ???
0x0004C390: 0x00a01013 ???
0x0004C394: 0x00064500 ???
The vendors ID are two for Cirrus Logic cards (GD-5440 and GD-5446) and one for a Trident TGUI-9685 (that just happens to have the same number as a TGUI-9660). I’m glad to see that
https://pci-ids.ucw.cz/ still exists! We used to bought discarded PCI cards with no labels, and use this site to discover what was it.
For my purposes, the GD-5440 is the easiest to get working, as it is basically a GD-5429 modified to have PCI bus. The PCI configuration space contains the headers for the cards in the slots. For the purposes of emulation (and in order to patch minimally the OS), I’ve implemented only a stub header for the video card.
I copied my GD-5429 driver almost directly, expecting it to fail when it was required. However, a port 0x0a79 access got me completely disoriented, a few minutes later, I remembered this was ISA Plug&Play. For a while, Microsoft determined a standard to create an auto-configuration protocol for ISA cards, so Windows 98 could detect automatically the card type. It was mostly used for sound cards, and then forgotten completely when PCI sound cards appeared.
I started by patching the write to ISA port 0x0a79, and then I saw how the code tried to read and I had to patch 0x020b, 0x020f, 0x0213… what the heck? I had to analyze the code to see that the code probes all ports starting from 0x020b and up to 0x0303. You can see this code at 0x00068980. If for some reason the OS crashes, it generates a whole ROM disassembly, a whole RAM disassembly, and a RAM dump.
And finally the moment came! My code tried to write to the PCI headers of the video card to enable it (I put a stub there), and then it tried to read a video register:
0x0006472C: 0x030382d4 CONST lr2,0x03d4
0x00064730: 0x02808200 CONSTH lr2,0x8000
0x00064734: 0x03008311 CONST lr3,0x0011
0x00064738: 0x1e418382 STORE 0,0x41,lr3,lr2
0x0006473C: 0x15828201 ADD lr2,lr2,0x01
0x00064740: 0x16518382 LOAD 0,0x51,lr3,lr2
0x00064744: 0x0b838300 EXBYTE lr3,lr3,0x00
This extremely hideous code is because the processor reads everything as a word, and then it needs to extract the byte from the PCI I/O word (the ISA code looked more legible *sigh*)
And finally it tried to write to the video memory:
CL-GD5440: Unhandled 32-bit write to 0x81000000
This means the whole video memory is in a linear map, and of course, it simplifies a lot the video emulation. Having linear video memory was a dream at the time because it also guaranteed faster access. I also got a different access type to the bitblt engine:
CL-GD5440: Unhandled 32-bit write to 0x800b8008
These are the same bitblt registers but mapped in a different way called MMIO (Memory Mapped I/O) using the old CGA address. I had to download the CL-GD5440 User’s Manual from
https://www.vgamuseum.info/index.php/cpu/item/143-cirrus-logic-cl-gd5440
After adding the memory handling, I reached the point where I could see the wallpaper. It looked nice! Although with a bug in the cursor color. This was because the data is written as a word to the PCI bus, with the byte in the place where the PCI card looks for the byte! But my code still expected the byte in the lower bits.
The first boot up of my windowed operating system with the emulated PCI video card.
It got stuck, and I was pretty sure a menu button should appear on the screen to run programs. I couldn’t find anything obvious, until I enabled the debug log again. It tried to changed the keyboard leds, and the status port didn’t returned a ready state. I put a stub, and then I could see the operating system bar at the top, and the letters were trash. Yes!!!!
I forgotten completely that the GD5440 chip could receive the bitmap for bitblt expansion directly through the main memory address. Once the bitblt is programmed for a bitmap expansion (with or without transparency), it disconnects the bus from the memory, and instead takes any access as bitmap data. You can write the bitmap in chunks of 32 bits at a time, and of course it was 4x faster than the old method of writing a single byte to RAM in Write Mode 4.
I had to separate the bitblt emulation and made it a simple state machine. When a memory access appears it is feed to the bitblt, expands it, and keeps working until the full rectangle is processed.
I only had to do a further correction in the access to the memory as 16-bit where it could draw only the left-side pixels because I applied the word mask before checking for high or low word.
A big hard drive! 80 mb
The windowed operating system has a hard-coded program menu that calls programs in predefined locations in the hard drive. I modified the buildboot.c program to create boot sectors with little endian byte order, and also to create hard disk images.
The original G11V2 computer used a 80 mb. SCSI hard drive that sounded like a plane turning engines on, however, for this resurrected demo I don’t need so much space. I preferred to limit it to 40 megabytes.
We need a minimum program to be able to handle everything in an easier way: Archivero. I started building a floppy disk image to be dragged and dropped in the hard disk image.
Now it is time to test if it works. Let’s build the floppy disk image along an empty hard disk image. At this point I decided the emulator should detect the computer type by the size of the input image, if you drop first an image bigger than 1.44 mb. it decides it is a G11V2 (so it can still work for emulating the G11V1 of my previous article). Oops! I forgot completely I didn’t made yet the storage and keyboard patch.
After coding the patch to call the SCSI emulation and handle the SDL keys untranslated, I lost almost 3 hours trying to discover why no sector read was made. This time, the file system wrapper called the SCSI initialization, as it didn’t found the SCSI card (the code I shown first) then it never tried to look for the drives. The solution: a single instruction CONST gr96,1 patched into the SCSI layer initialization.
I had a small bug in buildboot.c, it still built the FAT entries in big-endian format. So no file could be read. I also had to add conversion from UTF-8 to my local format, because most of my files have accents in its names.
I noticed also the 40 MB hard drive image was detected as 24,576 kb. (or around 24 MB). I went to the filesystem detection code:
0x0004E5C0: 0x15607d08 ADD gr96,gr125,0x08
0x0004E5C4: 0x16046060 LOAD 0,0x04,gr96,gr96
0x0004E5C8: 0x03316161 CONST gr97,0x3161
0x0004E5CC: 0x02476131 CONSTH gr97,0x4731
0x0004E5D0: 0x60606061 CPEQ gr96,gr96,gr97
0x0004E5D4: 0xa4006035 JMPF gr96,0x0004e6a8
0x0004E5D8: 0x15607d04 ADD gr96,gr125,0x04
0x0004E5DC: 0x16046060 LOAD 0,0x04,gr96,gr96
0x0004E5E0: 0x03616161 CONST gr97,0x6161
0x0004E5E4: 0x02706140 CONSTH gr97,0x7040
0x0004E5E8: 0x60606061 CPEQ gr96,gr96,gr97
0x0004E5EC: 0xa400602f JMPF gr96,0x0004e6a8
0x0004E5F0: 0x15607d10 ADD gr96,gr125,0x10
0x0004E5F4: 0x15618a40 ADD gr97,lr10,0x40
0x0004E5F8: 0x03006206 CONST gr98,0x0006
0x0004E5FC: 0x16046360 LOAD 0,0x04,gr99,gr96
0x0004E600: 0x15606004 ADD gr96,gr96,0x04
0x0004E604: 0x1e046361 STORE 0,0x04,gr99,gr97
0x0004E608: 0xb4ff62fd JMPFDEC gr98,0x0004e5fc
0x0004E60C: 0x15616104 ADD gr97,gr97,0x04
Not very helpful, it only detects the signature G11a (0x47313161) and the special NOP (0x70406161), then it copies eight words of data into the drive structure. It immediately gets the free space with this routine:
0x0004E7F4: 0x03008700 CONST lr7,0x0000
0x0004E7F8: 0xa800801b CALL lr0,0x0004e864
0x0004E7FC: 0x15829200 ADD lr2,lr18,0x00
0x0004E800: 0x61616000 CPEQ gr97,gr96,0x00
0x0004E804: 0xa4006106 JMPF gr97,0x0004e81c
0x0004E808: 0x15629270 ADD gr98,lr18,0x70
0x0004E80C: 0x16046362 LOAD 0,0x04,gr99,gr98
0x0004E810: 0xa4006303 JMPF gr99,0x0004e81c
0x0004E814: 0x70400101 NOP
0x0004E818: 0x1e048362 STORE 0,0x04,lr3,gr98
0x0004E81C: 0x8362611f SRL gr98,gr97,0x1f
0x0004E820: 0x14878762 ADD lr7,lr7,gr98
0x0004E824: 0xb4ff85f5 JMPFDEC lr5,0x0004e7f8
0x0004E828: 0x15838301 ADD lr3,lr3,0x01
You can see SRL gr98,gr97,0x1f and ADD lr7,lr7,gr98 to count the total number of zero blocks (free blocks). It reads an entry from the FAT using this subroutine:
0x0004E864: 0x25010120 SUB gr1,gr1,0x20
0x0004E868: 0x5e40017e ASGEU 0x40,gr1,gr126
0x0004E86C: 0x15810130 ADD lr1,gr1,0x30
0x0004E870: 0x15878a5c ADD lr7,lr10,0x5c
0x0004E874: 0x16048787 LOAD 0,0x04,lr7,lr7
0x0004E878: 0x08870087 CLZ lr7,lr7
0x0004E87C: 0x3587871f SUBR lr7,lr7,0x1f
0x0004E880: 0x80868b87 SLL lr6,lr11,lr7
0x0004E884: 0x15858a54 ADD lr5,lr10,0x54
0x0004E888: 0x16048585 LOAD 0,0x04,lr5,lr5
0x0004E88C: 0x15848a48 ADD lr4,lr10,0x48
0x0004E890: 0x16048484 LOAD 0,0x04,lr4,lr4
0x0004E894: 0x08840084 CLZ lr4,lr4
0x0004E898: 0x3583841f SUBR lr3,lr4,0x1f
0x0004E89C: 0x82868683 SRL lr6,lr6,lr3
0x0004E8A0: 0x15848a40 ADD lr4,lr10,0x40
0x0004E8A4: 0x16048484 LOAD 0,0x04,lr4,lr4
0x0004E8A8: 0x08840084 CLZ lr4,lr4
0x0004E8AC: 0x3584841f SUBR lr4,lr4,0x1f
0x0004E8B0: 0x82868684 SRL lr6,lr6,lr4
0x0004E8B4: 0x14848483 ADD lr4,lr4,lr3
0x0004E8B8: 0x14838685 ADD lr3,lr6,lr5
0x0004E8BC: 0xa8008017 CALL lr0,0x0004e918
0x0004E8C0: 0x15828a00 ADD lr2,lr10,0x00
My mistake now was pretty clear. The eighth word (offset 0x5c in the drive structure) should be the size of each FAT entry, while the fifth word (offset 0x50 in the drive structure) should be the pointer to the first directory block. I had interchanged places.
I updated buildboot.c with the corrections, and the hard disk image said correctly 40,932 kb. free.
Putting all together
With the C compiler, assembler, and text editor put together in the hard disk image (and the 1999 library), I tried to compile one of the operating system games. It compiled, and assembled, and then crashed. I reviewed the executable and it had “bugs”, like wrong instructions in the wrong places, and the first JMP instruction was replaced with a CONST instruction.
Maybe the string comparison in the assembler triggered a bug? I inserted log code in the emulator to see the input strings and the assembled instruction output, and it was right!
I remembered the binary was generated directly into a file, and then the assembler goes back to patch undefined labels. I saw an apparent bug on seeking back into the file.
Maybe the filesystem had a bug that had been corrected? I did a comparison of the 1999 version against the 2000 version, and no changes. In the process I made a few annotations of addresses:
- 0x0004ceb0 is the function table for the filesystem service (vector 0x48)
- 0x0004e520 is the function table for the G11a file system
- 0x00050a00 is the function table for the serial port services.
Anyways, I noticed the assembler patched several CALL instructions on a row, for anyone with knowledge of the Am29000, you cannot put together several CALL instructions because of the delay-slot. So the table was being built incorrectly.
Four days into looking for the bug, I finally inserted debug code into the fseek operation. Internally, the file system can handle 64-bit numbers (I really was thinking in the future), and I got a weird 0xffffffff in the upper word.
#define ALU(v1, v2, vc) \
if ((special[2] & 0x0400) == 0) { \
uint64_t tmp = v1 + v2 + vc; \
special[132] = (special[132] & ~0x0780) | (((uint32_t) tmp & 0x80000000u) >> 22); \
if (tmp > 0xfffffffful) \
special[132] = special[132] | 0x80; \
if (((uint32_t) tmp) == 0) \
special[132] = special[132] | 0x0100; \
}
Can you see the bug? The C language doesn’t expand automatically your type based on your input operands. Even if tmp is uint64_t, the operations are still done in uint32_t.
This is because in the assembler I had the following operation (haha, sorry, non-standard C):
fread(salida, &valor, 4);
fseek(salida, -4, 1);
It reads a word from the generated binary output, and moves the file pointer back to rewrite the word with the updated value. However, as the carry operation isn’t working, the file pointer was invalid, and the file system generated an error that although returned wasn’t processed because an operation like this cannot fail (famous last words).
I corrected immediately the emulator:
uint64_t tmp = (uint64_t) v1 + v2 + vc; \
And finally, my C compiler is alive again to compile Am29000 programs another day. I could compile easily the Bloques game, and it appeared in all its past glory. This program is available for compilation in the folder Entorno de desarrollo/Juegos/Blqoues.c.
The Internet is coming!
At this point of early 1999, I was pretty happy going to Ipsograph in Ciudad Satélite, our new Internet café after the demise of the one at Coapa. I used Internet most than ever, downloading documents and bring back floppies with these, along standards, and software that I wanted to test. I got the PDF standard and I coded a small PDF viewer that was incredibly useful to read the tons of datasheets that started appearing as PDF files.
The next big program I needed to code was increasingly clear in my future: A web browser. At the time, I used Netscape Navigator a lot, and I didn’t had Internet at home, so it wasn’t a high-priority in my list. Microsoft's Internet Explorer 3.0 started to being gifted everywhere, there was even people on the Ciudad Satélite mall giving away CDs. Truth to be told, it was a terrible and slow browser, and for a while Netscape still had the edge but they had to reduce their price to $29.95 USD, but I don't remember anyone selling Netscape copies in Mexico. It was already installed in Internet café's computers.
Around five years before of this, I had written a HTML viewer for the Z280 computer, and I coded a TCP/IP protocol stack in assembler language. But I couldn’t convince my father of getting an Internet subscription.
I knew Internet was getting into everything, so I took my old Z280 code, gave it a look, and I started coding my 32-bit Internet browser in March 22, 1999. The development was far more easy in C language, by April 9, 1999 I had a very simple HTML browser that I could run locally. I know that because I’ve the floppies with this early source code (you can find it inside the hard disk image)
I also was coding little by little the TCP/IP protocol stack and as it was the age of modems, also the PPP protocol (Point-to-Point Protocol), along PAP (Password-Authentication-Protocol), plus some AT commands to control the modem.
Todito Card for prepaid Internet access via modem. Circa 2001
The browser started being useful for reading the HTML files in CD-ROM discs we bought, and the network protocols were tested against a Linux box I configured myself with a PPP server (using a null serial cable). It was until June 24, 1999 when I managed to connect to Internet for the first time, using a modem, and a friend’s account in Prodigy.
I was astonished I could download my first file using my own software. I remember the radio at the time still played Bitter Sweet Symphony, and I had just got age 21.
This year, 1999, was the last time everything was so simple. Protocols started to evolve for more advanced requirements, and Javascript had just made its appearance and spread like fire.
Where is that browser?
Where I could find that browser from 1999? One big problem when you are developing things so fast is that you don’t stop to backup things. As I said before, I found some floppies with an early version of the Internet Browser, but no binary.
I had to look into my very old boxes, and then it was there, a dozen of CD backups that I made once or two times a year. I discovered sadly only two were still readable. One from 2001, and another from 2003. Another problem, all these are mini-CD, and these cannot be inserted into a Macbook Pro. I had to use an external CD drive.
My backup Mini-CD for 2001.
Now the good luck, I made backups inside the main directory of each project. So the web browser had the very early backup I saw on my floppy discs (April 4, 1999) and the second backup was the one I was looking for: November 11, 1999.
The executable for my Internet browser measured 362 KB. How this was fitted into 512 KB of RAM? I was somewhat puzzled, until I discovered I lost the time searching for a CD, because I already had the floppy disc with the file BIYUBI.ROM in the same disk where I got my 1999 windowed operating system. It never was loaded into RAM, instead the program was burnt into the EPROM.
The files for building a G11V2 ROM. The dates are incorrect as these were fixed in the disk operating system.
It is disk number 13 in my backups, it sounds appropiate for 1999 *chuckles*
The history went like this: The G11V1 computer was updated to 1 mb. of EPROM, and the upper 512 kb where filled with a startup sound (a marimba excerpt from a CD), so I removed this and burned the web browser in the same space, along the TCP/IP protocol stack.
So this means I’ve found my own holy grail: my first working web browser able to connect to the Internet.
Let’s boot that browser
Now I needed a small program to boot up the Internet browser. My OS has a small code to start the first task (the top bar with the menu):
0x0004B0B8: 0x03b082f0 CONST lr2,0xb0f0
0x0004B0BC: 0x02008204 CONSTH lr2,0x0004
0x0004B0C0: 0x03b18300 CONST lr3,0xb100
0x0004B0C4: 0x02008304 CONSTH lr3,0x0004
0x0004B0C8: 0x03ec8400 CONST lr4,0xec00
0x0004B0CC: 0x02bf84ff CONSTH lr4,0xbfff
0x0004B0D0: 0x03e08500 CONST lr5,0xe000
0x0004B0D4: 0x02bf85ff CONSTH lr5,0xbfff
0x0004B0D8: 0x03048600 CONST lr6,0x0400
0x0004B0DC: 0x030c8700 CONST lr7,0x0c00
0x0004B0E0: 0xa802802a CALL lr0,0x0004b988
0x0004B0E4: 0x0300791a CONST gr121,0x001a
The first argument in lr2 is the task name, the second argument in lr3 is the code location, lr4 and lr5 contain pointers to the pair of stacks required (remember the Am29000 has one stack for local variables, and another for bigger things), and lr6 and lr7 contains the size of these stacks.
This function is called internally when booting up an executable file. The files are made executable just by putting an attribute 0x0100, and the lower bits are used to mark hidden file, read-only file, and directory.
The executable header for starting up the web browser looks like this:
0x00000000: 0xa0000008 JMP *+8
0x00000004: 0x70406060 NOP
0x00000008: 0x00000030 ; Size in bytes of the program.
0x0000000c: 0x00000000 ; Space for zero-initialized variables.
0x00000010: 0x00006000 ; Size of the first stack (24K)
0x00000014: 0x00006000 ; Size of the second stack (24K)
0x00000018: 0x72420101 ; Call to OS
0x0000001c: 0x70400101 NOP
0x00000020: 0x03006000 CONST gr96,0x0000 ; Start the browser program from the ROM.
0x00000024: 0x02006008. CONSTH gr96,0x0008
0x00000028: 0xc0000060 JMPI gr96
0x0000002c: 0x70400101 NOP
The fact there is no further code doesn’t affect the operating system, as it will relinquish control with cooperative multitasking. The cooperative multitasking works in an unprotected environment, and it just saves the current PC for returning later (it doesn’t mind the browser code isn’t inside the original task loaded from the disk) So let’s type this.
Typing hexadecimal for creating a minimum executable for my OS.
It was almost 11pm when I discovered the minimum size for an executable program is 64 bytes, and that size should be also in the header, and I could get a glance of the web browser before it crashed.
After a whole day of debugging, I found it managed to show an error message before crashing. The message was “Protocolo desconocido en dirección” (unknown protocol in address).
Fortunately, I had the source code of the web browser, and I could track the first access to the homepage. It was an array called pagina_base[] (homepage) and following the assembler code, I could find it was expected in an absolute RAM address. Gotcha! I forgot completely about the data for the web browser.
The data area for the web browser was uninitialized! After some disassembly I could deduct it started at 0x80006980 for the web browser, and 0x80002980 for the TCP/IP stack. This also means I did an automated program to calculate relocations for fixed RAM position (where could it be?)
However, it was not so easy. Do you remember I used C language? There is initialized data that should be copied into RAM preceding the zero'ed area. After I took this in account, it worked!!! But the menus didn’t appear, after a small analysis I discovered the RAM was copied in a wrong place. I thought it was 0x80006980 when it should be 0x80006d80.
I tried to load a page, and it got stuck. Oh my! Why I made something so complicated!!!
Turns the browser tried to load a cache of bitmapped fonts, but I didn’t had the file at hand!!! I had to search for it in the Mini-CD backup, and fortunately I found the file “Cache de tipos” dated August 23, 1999. This file is composed of bitmap fonts I got from X/Window, and some pregenerated fonts made with the Type 1 rasterizer. This way the web pages displayed faster.
The typefaces cache file for my web browser.
Once this was in place (and in the right folder), my web browser went back to life for the first time in 27 years. And truth to be told, a tear dropped from my eye watching this again. It is like going back in time.
The browser is named Biyubi, after a Zapotec word meaning “search non-stop”. It was suggested by my uncle.
I made some further changes in buildboot.c so you get the exact dates these files were backup (frozen in time), and it creates directories automatically as I was losing time rebuild hard disk images when I found a bug. There is a script build_os.sh that takes all files from the 1999 directory, and rebuilds the hard disk image.
I was pretty sure I had a cache of old webpages that could be included, but I couldn’t find it, and besides it still could carry a copyright problem. So... Are you ready? Could it be possible, maybe, to get a last ride from this web browser?
Let’s connect this to Internet
There isn’t a lot you can do in the actual Internet with a web browser from 1999. The Internet has evolved several iterations with new protocols and standards.
The browser connected using my own TCP/IP protocol stack, but this software is tied to the use of a modem to get into the Internet, and a stack of point-to-point protocols (PPP, LCP, PAP, etc.)
It is way easier if I simply patched the network services to use directly the DNS and a translation layer for the TCP protocol. So I did that, I patched the network services so it pointed to an address table that in turn contained emulator traps. And for my web browser I only needed to resolve a DNS name, and access the TCP protocol.
This is the a portion of the code I worked very overnight.
case 0x15: /* resolver (solve DNS name) */
pc0 = REG_B;
c = regs[REG_AA(0x82)]; /* Get name */
{
struct addrinfo hints, *result, *rp;
int s;
char hostname[256];
char *ap;
ap = hostname;
while (ap < hostname + 255) {
*ap++ = read_byte(c);
c++;
}
*ap = '\0';
/* Returns -1 for non-existent */
/* Returns host order domain number */
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET; /* ipv4 */
hints.ai_socktype = SOCK_STREAM;
s = getaddrinfo(hostname, NULL, &hints, &result);
if (s != 0) {
regs[96] = -1;
} else {
struct sockaddr_in *ipv4;
rp = result;
ipv4 = (struct sockaddr_in *) rp->ai_addr;
regs[96] = ipv4->sin_addr.s_addr;
}
fprintf(stderr, "Solving %s to 0x%08x, returning to 0x%08x\n", hostname, regs[96], regs[REG_AA(0x80)]);
}
break;
case 0x1b: /* tcp_abrir */
pc0 = REG_B;
c = regs[REG_AA(0x82)]; /* Source port !!! */
d = regs[REG_AA(0x83)]; /* IP address */
e = regs[REG_AA(0x84)]; /* Target port */
{
int s;
struct sockaddr_in sserver;
s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) {
regs[96] = -1; /* !!! */
} else {
sserver.sin_family = AF_INET;
sserver.sin_addr.s_addr = d;
sserver.sin_port = htons(e);
if (connect(s, (struct sockaddr *) &sserver, sizeof(sserver)) != 0) {
close(s);
regs[96] = -1; /* !!! */
} else {
regs[96] = s;
}
}
fprintf(stderr, "tcp_abrir(0x%08x, 0x%08x, 0x%08x), returning 0x%08x\n", c, d, e, regs[96]);
}
break;
case 0x1d: /* tcp_leer */
pc0 = REG_B;
c = regs[REG_AA(0x82)]; /* Socket */
d = regs[REG_AA(0x83)]; /* Address */
e = regs[REG_AA(0x84)]; /* Bytes */
{
int s;
unsigned char *buffer;
buffer = malloc(e + 1);
s = c;
f = read(s, buffer, e);
if (f < 0) {
fprintf(stderr, "errno = %d\n", errno);
if (errno == EWOULDBLOCK || errno == EINTR)
f = -33; /* My OS value for EWOULDBLOCK */
else
f = -1;
} else {
for (e = 0; e < f; e++) {
write_byte(d, buffer[e]);
d++;
}
}
regs[96] = f;
fprintf(stderr, "tcp_leer(0x%08x, 0x%08x, 0x%08x), returning 0x%08x\n", c, d, e, regs[96]);
free(buffer);
}
break;
So far I've implemented it only for macOS, maybe later I’ll do the Windows sockets. It was pretty easy to solve the host name, and I did everything almost right in the first step, but it stopped short of reading the HTTP response, until I discovered my flush function (tcp_vaciar) was closing the socket because I did copy&paste.
It was exciting watching how my browser read the net again for the first time in 27 years.
My 1999 web browser visiting wiby.me in 2026.
What we have here
Download my
Am29000 emulator from Github, execute it, and drag&drop the
harddisk_master.img file inside the window (do it in the center of the window), you can also drag&drop further files to account for a removable floppy disk drive.
My windowed operating system (or Windows Fénix for short, later Sistema Fénix) in 1999 looked a lot more modern. The date is shown at the top-left corner of the screen (click it to change the date), there are four fixed icons: Volume (not working), Calculator, System Status, and change screen resolution (not working). On the top-right corner of the screen is a button for displaying a fixed menu of programs. You can double-click title bars to minimize windows.
The only working programs are Ajedrez, Archivero, Fénix C, Circuito Impreso, Publivisión, and Bloques (just compile it from source using Fénix C)
You can also print source code to any of the supported printers. I already configured the printing for using free fonts (located in Sistema/Tipos de letra). By the way, I had a crash trying to print until I remembered the system requires the Sistema/Temporal folder to create temporary files.
For HP LaserJet IIP printers you can see the generated printer.txt document using
redtitan.org.
A source code file printed with Fénix C for HP LaserJet IIP.
The source code for the C compiler and the assembler are in the Entorno de Desarrollo folder. Did you notice I recommended drag&drop the hard disk image in the center of the window? If you want to recompile the C compiler or the assembler, you need extra memory (disabling the web browser), to do this drag&drop the hard disk image file into the bottom-right corner of the emulator window (use the Promedio utility to see the free memory). It is pretty amazing to watch the 10,203 lines of source code being compiled and getting exactly the same binary.
In fact for compiling again the binary for my old version of Publivision (I did this myself, no source code in the git, yet), there is a further trick: closing the editor window. Otherwise the compiler lacks memory. Publivisión is almost the first working version from the last day of 1998, so it is filled with bugs, anyway you can create documents with it and print them. This early application already runs around 10,000 lines of source code.
Circuito Impreso is my PCB editor and it is the most polished application at the time. It is pretty easy to use, just experiment with left click (draw) and right click (select). The credits image was scanned from an AMD manual cover. This program is around 9,000 lines of source code. There are a few bugs in the display driver when moving items, but I'll correct it later.
To run the web browser, open a file browser (Archivero), and click in Explorador de Internet. I was wobbling between naming it Explorador or Navegador (Netscape wasn't fond of anyone saying Navigator). I also put the source code to my very first browser (more like a viewer), and I don’t know if it can be compiled, but probably it would need some changes. I’ve the source code for the version in ROM, but it lacks the adapted JPEG library. I’ll consider whether I publish it incomplete or if I remake the JPEG library. If you remember, my C compiler didn’t yet had a linker, so I modified the JPEG library to be able to include its individual files. With so many files, it took several minutes to get a new compilation of the browser!
Another thing you’ll notice in the web browser, I’m still not emulating the Am29050 processor, and the JPEG library depends a lot on the multiplication instruction, so it is incredibly slow for displaying JPEG images. It is so 1999!
Postmortem
I had an idea of what I was doing, but I was more driven by the excitement of the discovery about learning how to do things. And at the same time, I simply did something that was required at the time. A windowed operating system, a development environment (text editor, C compiler, and assembler), a desktop publishing program, a printed circuit board editor, and a web browser. An innovation ages away from my transputer operating system.
I didn't notice when starting this article, but after reading it over, I mean, I coded close to 50,000 lines of source code in one year!
It was starting to be competitive, and it was because I put my own ideas for interfaces, and I optimized the things a lot in order to fit a small machine. The next year, 2000, my programs looked a lot better, I made them stable, and a lot more professional. The C compiler got a linker, so I didn’t need to compile 10,000 lines of source code just for a little change. My browser supported Javascript. I was almost on par with the browsers at the time. We got many interviews that year, even one on radio Radioactivo 98.5 that was very famous at the time.
We used the chips we had available. The Am29000 was showing its age, we moved already to the Am29050 processor. The G11V3 computer had a lot more memory, it was clocked faster, and with hardware changes it could use the extra memory available in the PCI video card. I started to implement CSS in my web browser. But that's an history for another article.
I’m a freelance developer and I work hard. Writing these articles uses a lot of my time, and I enjoy it. But I would be a lot better if you support me with my suggested $9 USD per month in
Ko-Fi (for sure you go to the movies once a month, but these articles are better than many movies!). Support nanochess, you’ll get good karma, and I’ll be eternally grateful! You can buy also my books in
Lulu.com, my ebooks and games in
my digital store.
Related links
Última actualización: 16-ago-2026