Titanium Whitehorse
Introduction#
One of my more ambitious projects was to develop my own computer system for Minecraft. After about a year or so of back and forth planning, developing, and undoing everything I’ve spent months on, I’ve finally gotten close to something of value and most importantly: it works. After all this time, tangible progress has been made and I’ve done most of the current work in about a week, everything from the months upon months of work on the previous iterations not included, no longer relevant. The decision to branch into a custom architecture instead of some kind of scripting language was partly due to my interest in the low-level side of programming and my belief that it would be simpler to implement, since you could have each game tick of the host TileEntity be a clock cycle for the CPU within. Though I suppose you could increase the clock speed from a pitiful 20 Hz by increasing the amount of clock cycles executed per game tick either by loop or explicitly inserting them, I don’t know how much higher I’d want to go, at least in Minecraft, but especially modded Minecraft, where people may be running hundreds of other bloated and unoptimized mods and/or just simply have multiple in-game computers running simultaneously.
I also felt that it would be easier to develop my own simplistic architecture than to copy a preexisting one and that reflects in the final product, with each instruction being a single byte and the only things being larger than bytes being literal values. The first attempts at implementing an interpreter/emulator for the CPU were overengineered, to say the least. If you thought the current iteration was overcomplicated, be glad you never had to see, let alone work on any of those previous versions. I do like this final one though, almost all instructions are implemented using a single case line. There are a few helper and utility methods that facilitate that, but I think that helps when I decide to extend the architecture and don’t want to just copy-paste the entire interpreter and change all instances of readShort(), writeShort(), or whatever into int and long equivalents.
At the very least, I thought it’d be both pretty cool to have in-game and a good learning experience for myself. For the longest time I never really could wrap my head around the low-level, computer hardware aspect of computing. It was like magic, that a tiny metallic square piece of metal could perform all these things, so fast. After discovering the intricacies of assembly, I was yet again dumbfounded. Though this time I understood that anything high-level could ultimately be broken down into simpler and simpler tasks, eventually assembly instructions, I didn’t understand how any sane human could comprehend the concepts well enough to program and work with complicated algorithms and data structures just in assembly. After some years, I’ve come to the following belief: they weren’t sane at all; that’s when it all began to make sense again.
Getting back on topic, I think I’ll have to confess and admit that all of this is to some degree just for my own benefit, not NTM’s or anyone else’s. Maybe I did initially, but after a while of developing a brand new assembly language and observing the general folk on the NTM Discord, I have to be realistic; it couldn’t take the combined intellectual capacity of every single one of those depraved, cognitively deficient, freaks couldn’t figure a way out of a cardboard bag1, let alone figure out said CPU architecture, but I digress.
Though it should also be noted, the reason behind adding computers isn’t so much of a “just ‘cause” development from my own whims, rather a desire to completely rework how NTM’s progression works (let’s call that project Aeneous Tenrec). It was going to be deeply related to computers, so I thought it’d be better to start developing in-depth computers themselves before making them used in the mod’s progression. Besides, I don’t want to drop a huge update on Bob to merge or on the playerbase too soon, so a more “held back” version would be submitted instead. Crafting recipes, balancing, and integration into the progress chain wouldn’t be implemented yet, since it would both give me a break from the project and allow the playerbase to get used to it. Machine interfacing and some other basic built-in utilities would still be built-in, but most of the complex features wouldn’t be included yet, this makes the job of merging easier for Bob and learning how to program the CPU and all of its quirks easier for the playerbase with a smaller scope to deal with. Then, once the big features are introduced, the players already have some experience in working with the computer system used to operate those features. And so, with all of that out of the way, let’s begin the story of Zephyr Solutions’ iZ16 CPU!
Birth of the iZ16 Architecture#
Emulating a CPU was a more ideal (and intriguing) solution to my predicament for multiple reasons. Obviously, it both solves the issue of having to restrict execution and be able to be programmed from within the game itself. It also solves a less obvious issue of performance cost, at least for now, since each tick, the emulator only has to perform a few comparatively very simple tasks2. Even some simpler machines in NTM can have some fairly complicated per-tick processes and yet there can be many of them running simultaneously, without any lag. Of course, I still wouldn’t want to push it too far, so for the time being, I believed the clock rate of 20 Hz would be sufficient, at least to test the waters a bit more.
The first step would be to construct my architecture’s instruction set. Since I had no experience in assembly beforehand, I decided to (vaguely) base it off of something I thought would be appropriate. The Zilog eZ80 microcontroller, probably most known for being used as the CPU in the TI-84 Plus CE calculator (of which I have and like very much). Its user manual can be easily found online and it has a section detailing the instruction set. This made the process of developing a bit easier, though some instructions I didn’t add either because I didn’t think they’d be necessary or just not relevant for my CPU3. In fact, Zilog and their eZ80 are the main inspiration between for the fictional company that developed and primarily produces the CPU and the CPU’s name itself. “Zephyr” is clearly a play on “Zilog” and “iZ16” one on “eZ80”.4 (though I’m actually not sure if this means Zephyr Solutions in my headcanon is Zilog or is just another, similar company)5.
After toiling for a while, trying to fit the instructions in a sequential list, I decided to ditch that idea and opted for an opcode table (pun intended) to organize the instruction set. Here, I was allowed a visual representation of how to categorize instructions and see how much space I had left. It worked out well enough such that most instructions fit in a general category that can be determined by the upper nybble, though there are a few exceptions. Probably not particularly useful unless you find yourself debugging machine code often, but I’m quite satisfied with it. With all6 my instructions laid out before me, it made the process of designing and implementing behavior much easier7. Though, the growing pains aren’t quite over yet, as I still had to develop that subpar iteration of the emulator I mentioned.
Precursors#
Non-Assembly#
Get ready for some backstory because the main precursor to the current iteration wasn’t particularly impressive and has little to nothing with the current iteration. As stated, it was overengineered and couldn’t do much. Most importantly, there was no way to program it. Everything had to be pre-programmed, either by me or someone else through an API. The only functions and programs that I made were pretty basic, an echo command, a command to get the computer’s UUID, and a high-precision calculator, the latter of which I didn’t even code the calculator, I just reused the GUI calculator, which I also reprogrammed to use BigDecimal instead of plain ol’ double. The issue of true programs that would be expected to run longer than instantly is that the game would try to execute the whole thing, which would hold up the rest of the game on that thread.
My first thoughts to get around this was to put each program in a thread and incrementally run and halt it until done. The issues with that were that you can’t really be sure of how long you want to execute the thread and the thread itself is quite costly. The other “solution” was to divide the program into a huge switch statement, with each case being a statement. Control code would basically be goto statements to change the statement number. The issues with that being more obvious:
- It’s hideous and I would not want to work on it.
- Branching and control logic would be very difficult, especially if you change the program a lot.
- “Functions” would also be difficult.
- External functions would not be subject to throttled execution.
- Still doesn’t solve the issue of the system not being programmable.
That “solution” in particular made me realize something that leads into the current iteration. The whole “going through each statement of code, line by line, one at a time”, is just assembly. So why not stop beating around the bush and just go ahead with something that would actually work with that idea. Thus that, and a few videos I saw on YouTube about the same thing (custom CPU architectures, that is), set me off to take this project upon myself. I wouldn’t say it ended up easy, but it wasn’t quite as hard as I would’ve thought.
(Also as a note: this was developed long before the iZ16 concept was even conceived.)
The First Attempt at an Emulator#
The draining, overwhelming, and ultimately pointless venture that was the first attempt at an emulator was a nightmare for sure. Just about the only thing useful to come of it was that when it finally dawned to me, I was basically yelling: “this is horrible, why did I ever imagine this was a good idea at an attempt?” to myself. I am also glad to say that I delete all traces of the emulator and all associated classes from my project and computer as soon as the second iteration proved to be actually functional. I wouldn’t dare put a fraction of that code on the internet8. I still remember a good amount of it though, but to explain it all, it’d take away from explaining the proper version, so maybe I’ll write another page for it and link it here, but for now, I’ll just summarize everything.
At its core, structurally and conceptually, the two versions are actually quite similar. They differ mostly in the implementation of those function structures and concepts. I like to call this version: “abstraction fixes everything”9. Indeed, almost everything was needlessly wrapped in either some abstract class or interface, much to the probable horror of those who avoid OOP concepts. Sure, it might’ve made it easier to update the code without having to write more boiler-plate code or different methods for slightly different cases in theory, but in practice, I assume the abstraction would just make it a horrorshow to debug with the redirections obfuscating the concrete behavior and kill performance just with all the function jumps and polymorphism. Worst of offender of all this, I think, were the registers.
The iZ16 has several general purpose regsiters, of varying sizes. There are 8 single-bit conditional registers, able to be used by both the programmer for easy booleans and instructions for updating statues, 4 8-bit (byte) registers, for smaller numbers, 8 16-bit (short) registers for general purpose use, and 2 32-bit (int) registers for special use cases (and so I don’t have to implement a half-float in software). That’s 4 different register widths, but another quirk with the CPU’s architecture is that any (applicable) instruction can use any register size. You can use a multi-bit register as a boolean for a conditional instruction much the same way you can use a boolean register as an integral value in an arithmetic instruction. This makes it a easier write code, since you don’t have to worry about specific instructions for specific types of registers used, but for me at the time, this was a complication in the interpreter. My solution? A little interface called IValue and its offspring ISettableValue, whose purposes should be self-evident, as well as the upcoming issues.
At the time, it was perfectly logical to me. It had methods for automatically casting the value to all possible primitive types and the writable extension has similar methods for setting the underlying value, much the same way. Calling asBoolean() or asShort() worked the same way whether the underlying value was a boolean, short, int, or even a float (when I said, “all possible primitive types”, I meant all primitive types, but we’ll get to that) and the writing versions were the same. A single register could be represented as one of these and all stored together using the interface as the type in an array, so when called, the underlying type doesn’t matter, and the instruction will execute normally. Perfectly, logical.
The Horror of the FLAGS Register#
While the excessive use of abstraction indeed bloated out the main instruction fetch and execution function, it still would’ve have been functional, in theory at least, there was one feature that would not optimize well is the “FLAGS” register. Certain architectures like x86 or (e)Z80 have a special register called FLAGS (or EFLAGS and RFLAGS for 32 and 64-bit extensions of x86, respectively) that basically keeps track of certain statuses of the CPU, but mostly keeping track of the state of arithmetic results. That means, with every arithmetic instruction, the emulator would have to determine which bits to set or reset in the register, even if they weren’t going to be used. This would mean a lot of wasted time on things the emulator would have to perform, for the only reasons of “it’s a common feature in CPUs” and “it might be useful in 0.1% of instances” or something along those lines.
Another issue is trying to calculate those conditions in the first place. At the hardware level, it would be arbitrary addition, since the statuses would be basically free to pass from the ALU’s transistors into a register, very easy, basically free of charge, sort of speak. However, not so easy in high level languages, distant from the hardware level, especially like one in Java, that isn’t even directly running on the bare metal, rather its virtual machine. Some flags, like the parity flag, would be easy, just a simple parity check on the result, which is traditionally done via a modulus operation like: (num % 2) == 0 to check if even, but it can be optimized to be a simple bit mask, since binary is nice like that sometimes: (num & 1) == 0; and similar is the sign flag, just with a different position to mask, or the zero flag, which is just a zero check. Others, like the carry, auxiliary/half-carry, or overflow flags are more difficult to determine in an efficient matter.
It got to the point where the code to calculate the flags became a method so large, that it might’ve rivalled parts of the interpreter itself. Not to mention, that I revised it several times to try and make it more efficient or cover more of the flags. Ultimately, it proved to be a pointless feat, since it was suggested that I just ditch FLAGS altogether. I suppose a better solution would be to save the operands and result of each arithmetic operation and only calculate a specific flag when requested, but I was told that FLAGS wouldn’t really be missed on a simple CPU architecture for Minecraft of all things, I sure wouldn’t miss it. A sort of remnant that I and others agreed on to keep was a TST (TEST) instruction, which is categorized with the logical bit instructions. It’s like a manual way to set the FLAGS register, but based on the result of a logical AND operation on two values. This way at least, you’d have to specifically request it and it only sets two flags anyway. It doesn’t even use its own register anymore, it just reuses the conditional bit register, but this was finalized in the final iteration, so I’ll get on to that already.
Development#
One of the more notable difficulties wasn’t even with the design of the architecture itself or its implementation per se, it was an issue with the Java language, that the interpreter/emulator had to be written in since it’s in Minecraft (writing it in C and linking it to Minecraft via JNI would be excessively complicated and would likely be even slower than a native Java implementation). Java doesn’t have unsigned primitives, so integers with their most significant bit set are viewed as negative and upcasting them will perform an implicit sign extension, which you will have to undo with bit masking. Java gets a lot of flak for a variety of reasons, oddly this is one not mentioned often.
Constantly performing bit masks to prevent sign extension and trying to keep track of if you want signed or unsigned division (as an example, since the other basic arithmetic operations are the same regardless of signedness) gets tricky, especially with the density of the code, which has led to other problems as well. Having to use a method to perform unsigned division is also an annoyance, though at least any possible performance hit will probably be optimized away during compilation.
The first program I wrote was done in its native machine code, as it was written before I made the assembler. All it did was output some integer values through the CPU’s I/O ports, which were caught and displayed using some console printing in the faux motherboard implementation code. It was proof that my efforts weren’t for naught, but I’d need that assembler if I wanted to progress any further in making test programs, as going back and forth between a byte array in the program and the opcode table, is not ideal
The assembler was actually deceptively simple to write, I thought it’d be more complicated, probably from my previous attempts at making the interpreter and my misguided belief that everything could improved if I just added more abstraction layers, more objects, and more complications.
Footnotes#
- Because it would be an egg.
- The whole thing sort of turned into a RISC I suppose.
- The eZ80 is a 24-bit CPU, which is interesting in itself, but it has a compatibility mode for the preceeding Z80 CPU, which was only 8-bit. Though, this does show you can do impressive things even with such limited hardware.
- Zephyr is just a cool word I thought of, the letter ‘Z’ the most obvious connection to its inspiration. iZ16 has a bit more reason to its rhyme. The lowercase ‘i’ might just seem to be a riff on the lowercase ’e’, but it actually stands for instruction set. The ‘Z’, of course, represents Zephyr and finally, the “16” represents the data with of the CPU, 16-bit. So in full, the name is effectively “instruction set, Zephyr Solutions, 16-bit”, which almost sounds like something the US military would come up with for a tank or gun designation. The CPU is named after the instruction set, sort of the opposite of the Intel 8086.
- Extensions are denoted with the ‘E’ suffix and an additional suffix to specify. For instance:
iZ16E-32would be the 32-bit extension.
- Extensions are denoted with the ‘E’ suffix and an additional suffix to specify. For instance:
- I’ve decided to avoid referencing any real companies or corporations in this project, mostly because for reasons of confusion, but it might be helpful for avoiding legal issues as well.
- Or rather, most, as I added a few after the main bunch, either because I forgot (happens often) or recommendation from others.
- I write everything in the in-universe manual, so I could be productive and have fun at the same time.
- Though, it would be an amusing thought for an AI to steal it and have it single-handedly demolish any hope of it being able to produce competent code.
- This is actually a common mistake a lot of people that use(d) OOP languages make, I’ve come to notice. I should write about that too eventually.