This will not be a post about programming unfortunately, so I will keep this very short.
If you are a political figure and currently have a respectable following of people that listen to you, you should take heed. Noise does nothing but dilute the signal. The more noise, the less you can make out of the signal. When noise is filtered out so is a portion of the signal.
In case you have not figured it out yet, the signal is your political agenda. The noise is you blowing small issues out of proportion.
Casting Rays
A blog about graphics and programming, sometimes combined.
Tuesday, July 7, 2015
Wednesday, October 15, 2014
Mathematical Expression Parsing and Evaluation Made Easy
Disclaimer
This post is about how to parse and evaluate mathematical expressions. I assume there are several libraries built to enable you to do this already. I'm not particularly well versed in regular expressions, but I assume many implementations probably provide functions for this as well. This post, however, aims to explain how mathematical expression parsing and evaluation works even if you probably don't want to implement it yourself so as to provide you with a new level of understanding.
I should also mention that the code presented below will likely not be the most capable of all mathematical expression evaluators, nor the most performant. It is however, a neat way to think of the problem and trivializes parsing and evaluation of mathematical expressions.
Finally, there might be flaws in my logic. I am human after all. The algorithm as presented below seems to work for all test cases I have given it, but supposing I was stuck in a rut when coming up with test cases I might have categorically missed cases where my algorithm pathologically fails. If so, I am sorry. Hopefully, the idea presented should come through clearly enough regardless.
Introduction
As a programmer, you most likely know what a mathematical expression is. Hell, as a middle school graduate you probably know what a mathematical expression is (although I admit that if you are not regularly performing math in your adult life the terminology might have slipped your mind by now).
Simply put, a mathematical expression is a set of terms combined by a set of operations. Something as simple as this is an expression:
a + b * c
And something as complex (or more) as this is an expression:
(ex * 2x+1) / x!
The naïve approach to expression parsing would be to simply step through a string and evaluate. The problem faced when doing that approach is the order of operations. The order of operations observed is generally PEMDAS, meaning parenthesis are always evaluated first (writer's note; parenthesis are not mathematical operations in and of themselves, merely a way to escape the order of operations otherwise imposed in evaluation); exponents are evaluated second; multiplication and division are evaluated third, but have equal standing in relation to each other; addition and subtraction are evaluated last, but also have equal standing in relation to each other. Operations having equal standing simply means that those operations are executed in the order they are encountered from left to right since there is no internal preference between them.
I should note that sometimes there are ways to stylize a mathematical expression so as to escape PEMDAS. Commonly, division is used in such a way;
a + b
c - b
unrolls into (a + b) / (c - d) rather than a + (b / c) – d despite the lack of parenthesis. We are not going to be handling these cases as code complexity would swell by quite a bit and obscure the relatively simple principles that lie underneath the approach taken in this post.
The idea
As was mentioned before, some algorithms might seek to just parse a string left to right in order to evaluate a mathematical expression. However, this might break the order of operations. Some other algorithms parse left to right but try to be more aware of the context and might jump forwards and backwards in the string in order to evaluate the expression correctly. This is a needlessly obtuse approach even if it might work. In fact, it is probably a similar approach to the one I am about to present but without the use of a very important data structure that aids the functioning of the algorithm immensely.
Consider the following expression:
(a + b) + c * d^x
If we de-construct the terms into a logical order of binary operations we should get the following:
A = a + b
B = d^x
C = c * A
D = A + C
Where the result will be stored in D. Thinking about this a little harder we can see that we only use at most two evaluated operands at a time. We are also recursively evaluating each term. With this in mind, that same sequence can be expressed as a diagram:
Hopefully by now, the use of data structure should be evident; It is the trusty old binary tree. However, within the this field of science it is most often referred to as a syntax tree. Evaluating the expression is no more than traversing the tree in order and performing the operations on the operands given. The astute might have noticed that the trees can become quite unbalanced depending on the expression. However, this does not matter in the slightest for our purposes. I will explain why shortly. In order to do that though, we need to address the question of how the tree is constructed in the first place, and then parsed.
In essence, for a given node and a string containing an expression we need to parse for the least significant operation (discounting the contents inside parenthesis) and then split the string containing the expression at that operation into a left side and a right side while the operation itself is stored at the current node, making it an operation node. We then recursively perform this logic on the left and right child nodes of the current node, giving the left and right string split as input to those recursive calls respectively. Note that, as mentioned before, parenthesis is not an operation per se, which is why we are not parsing for it at this stage. Because of this, we are only looking for the EMDAS in PEMDAS. I will come back to the question of why we are reversing the search for operations as it is related to how we traverse the tree and evaluate its contents.
If we can not locate any operation in the expression we are processing then the expression might be a parenthesis. In that case, remove the outer parenthesis and make another recursive call to our logic given our current node and newly formed string as input.
If an input expression neither has an operation in it, nor is a parenthesis, then it must be a value. This brings us to the third and final case to test for, and constitutes our stopping condition; the current node is marked as a value node. Simply put, try to convert the expression to a type your of choice (usually an integer or a real value). If the conversion succeeds, store the value in the current node. If the conversion fails then maybe it is a symbolic constant. In that case look the value up and store it in the current node. Otherwise, something is amiss and the algorithm generates an error.
Since we are constructing the tree without having to leave the current node and then insert a new one by traversing the tree down a branch this is an O(1) operation. In total however, we process n items, where n is the number of operations and operands combined, making the construction of the tree an O(n) operation in total.
Evaluating the tree is nothing more than traversing the tree by requesting the value stored at a node. If the node is a value node, it simply returns the value that is stored at that node without asking any further questions. If, on the other hand, the node is an operation node it will have to query its left and right child nodes for their values and storing the results inside a left-hand-side variable and a right-hand-side variable respectively. Finally it returns the combined LHS and RHS based on what operation is stored inside the node.
Knowing this, the answer to why we are parsing for the operations in reverse order, i.e. addition/subtraction, then multiplication/division, and finally exponents, should be fairly obvious as the topmost (as opposed to the root of the tree) operations stored in the tree are the first operations performed during a traversal of the tree. That means that the most significant operations must be stored at the top in order for them to take precedence over lesser significant operations. This is done by adding them to the tree last. However, reverse search does not end there. We also have to parse the expression string from right to left so that operations of the same rank are evaluated left to right. PEMDAS sets up an exception to the left-to-right rule; exponents (many applications screw this exception up, such as Excel, GNU Octave, and my first draft of this article). Exponents are parsed right-to-left, meaning the expression 3^4^5 evaluates to 3^(4^5), not (3^4)^5 as would be the case with 3/4*5.
Since we have to move through all of the elements in our tree to evaluate the expression we get an O(n) time complexity. This brings me back to the initial concern about unbalanced trees and why it is not a problem for our purposes. Constructing the tree is an O(n) operation, while evaluating the expression (traversing the tree) is also an O(n) operation, meaning the entire process is O(n) + O(n) or O(2n), simplified to O(n).
Since we have to move through all of the elements in our tree to evaluate the expression we get an O(n) time complexity. This brings me back to the initial concern about unbalanced trees and why it is not a problem for our purposes. Constructing the tree is an O(n) operation, while evaluating the expression (traversing the tree) is also an O(n) operation, meaning the entire process is O(n) + O(n) or O(2n), simplified to O(n).
The implementation
For this post I will be using C++, but feel free to implement this in any language you like. The logic still stays the same regardless. The general outline of the data structure used:
class Expression
{
private:
std::string m_expression;
float m_result;
Node *m_root;
private:
bool IsTermBalanced( void ) const;
bool IsBraceBalanced( void ) const;
void SanitizeExpression( void );
void DestroyTermTree(Node *node);
void GenerateTermTree(Node *& node, const std::string &expression);
size_t FindOperationLeftToRight(const std::string &operations, const std::string &expression) const;
size_t FindOperationRightToLeft(const std::string &operations, const std::string &expression) const;
public:
Expression( void );
~Expression( void );
bool SetExpression(const std::string &expression);
float Evaluate( void ) const;
};
As a side node, I was arguing with myself about how to implement this algorithm, mainly regarding the somewhat dubious question of whether the most elegant implementation is a class with functions, or just a collection of functions. I came to the conclusions that just using functions would be incredibly elegant, yet somewhat at odds with the design philosophy as championed by C++, at which point I decided to implement the algorithm as a class.
The presented binary tree is somewhat unorthodox. Some nodes are values, some nodes are operations. This might pose some difficulty when implementing the tree given certain languages, but there should be ways to work around this. In C++ we can solve the problem using polymorphism. In C, we could simply make a generic node structure and attach a state flag indicating whether the node is a value or an operation.
struct Node
{
Node *left;
Node *right;
virtual ~Node( void ) {}
virtual float Evaluate( void ) const = 0;
};
struct Operation : public Node
{
char operation;
float Evaluate( void ) const;
};
struct Value : public Node
{
float value;
float Evaluate( void ) const;
};
Note, that the end user will never actually handle this data structure. Making it local to a compilation unit makes sense, meaning you can forward declare it in your header file while actually declaring it in you source file.
Since we described the topic by covering the creation of the tree first we will do so here as well. The function is simply called SetExpression, but the main workhorse is the recursive call to GenerateTermTree.
bool Expression::SetExpression(const std::string &expression)
{
m_expression = expression;
SanitizeExpression();
bool retVal = IsBraceBalanced() && IsTermBalanced();
if (!retVal) {
m_expression.clear();
}
DestroyTermTree(m_root);
GenerateTermTree(m_root, m_expression);
return retVal;
}
void Expression::GenerateTermTree(Node *& node, const std::string &expression)
{
const std::string Pemdas[] = { "+-", "*/", "^" }; // reverse the order
size_t opIndex = std::string::npos;
char op;
for (int i = 0; i < 3; ++i) {
if (Pemdas[i] != "^") {
opIndex = FindOperationRightToLeft(Pemdas[i], expression);
} else {
opIndex = FindOperationLeftToRight(Pemdas[i], expression);
}
if (opIndex != std::string::npos) {
op = expression[opIndex];
break;
}
}
if (opIndex != std::string::npos) {
Operation *opNode = new Operation;
opNode->operation = op;
opNode->left = NULL;
opNode->right = NULL;
std::string lexpr = expression.substr(0, opIndex);
std::string rexpr = expression.substr(opIndex + 1, expression.size() - opIndex - 1);
GenerateTermTree(opNode->left, lexpr);
GenerateTermTree(opNode->right, rexpr);
node = opNode;
} else if (expression[0] == '(' && expression[expression.size() - 1] == ')') {
GenerateTermTree(node, expression.substr(1, expression.size() - 2));
} else { // STOPPING CONDITION
Value *valNode = new Value;
valNode->left = NULL;
valNode->right = NULL;
valNode->value = atof(expression.c_str());
node = valNode;
}
}
You may have noticed that this implementation performs some functions that we never mentioned in the previous section since they are not inherently associated with the data structure or algorithm. As such I will not go into exact detail about how these functions do what they are supposedly doing, but I will give a brief overview.
- SanitizeString simply removes all kinds of whitespaces from the expression string.
- IsBraceBalanced makes sure that each opening parenthesis corresponds to a closing parenthesis.
- IsTermBalanced makes sure that each operation has exactly two operands.
- FindOperation linearly parses the string looking for one of the specified operators, stopping at the first one. The trick to this function is to increment a counter for every opening brace encountered and decrement that same counter for every closing brace encountered. While the counter is not 0, skip detecting operators. The variants *LeftToRight and *RightToLeft searches for the characters incrementing from the first to the last character in the expression or decrementing from the last to the first character respectively.
- DestroyTermTree frees the memory used by the expression data structure.
And now the evaluation. Most of the code is done in the node's virtual Evaluate function, while the tree's Evaluate merely calls upon the root node's Evaluate function:
float Operation::Evaluate( void ) const
{
float lval = left->Evaluate();
float rval = right->Evaluate();
float result;
switch (operation) {
case '+':
result = lval + rval;
break;
case '-':
result = lval - rval;
break;
case '*':
result = lval * rval;
break;
case '/':
result = lval / rval;
break;
case '^':
result = pow(lval, rval);
break;
}
return result;
}
float Value::Evaluate( void ) const
{
return value;
}
float Expression::Evaluate( void ) const
{
float result = 0.0f;
if (m_root != NULL) {
result = m_root->Evaluate();
}
return result;
}
With this we have now covered the bulk of the algorithm as well as the data structure, but your implementation could just as well look very different. There is still some room for improvement though.
Further improvements
Reading this post, and being somewhat well versed in mathematics, you might go 'Hold on! What about unary operators!?' Your concern is legitimate. The tree structure does not fit perfectly for unary operators, or functions for that matter. It could however quite trivially be extended to handle these cases. On the one hand, there is the unary operator -, as in -x. In reality though you can substitute this for (0-x). Other unary operators such as ! are harder to handle, but not impossible. Unary operators and functions could be implemented by adding a new node type that does not branch (or for simplicity's sake consistently only leads down the left or right pointer to another node).
The code as presented above does not have support for symbolic constants, making that an obvious point of improvement. That should not be more difficult than doing a string lookup if the conversion to a floating point number fails. However, that will require another string to floating point value conversion function as atof will always return a valid value (0.0 on failure).
Tuesday, September 2, 2014
SDL 1.2: "Failed to initialize video: No available video device"
I recently helped a person wanting to run cndoom 2.0, a fork of Chocolate Doom 2.0 geared towards speedrunners, but who was prohibited from doing so as the program would silently crash on startup (the only information I have is that it concerned a Windows 8.1 machine with latest drivers and DX versions). Only a cryptic error message was logged in a log file ('Failed to initialize GUI'). With a little help from acquaintances I managed to get a hold of a copy of the source code for cndoom 2.0 (usually kept secret to ensure security from speedrunners) and found out that this error occurred in InitTextscreen (src/setup/mainmenu.c) due to a call to TXT_Init (textscreen/txt_sdl.c). The takeaway, however, was that SDL_Init and SDL_InitSubsystem would fail (cndoom uses SDL 1.2). This is pretty strange behavior; I've never seen SDL fail at SDL_Init before.
After some poking around in the source code I deduced that SDL, or maybe cndoom, failed to detect a video driver and then failed to set a fallback driver (in SDL, the driver is detected via the SDL_getenv function). Using the SDL documentation as reference, I found that, under Windows, there are two drivers that can be used; windib, and directx. Just as I was about to force the video_driver environment variable via SDL_setenv, the person experiencing the problem detected that the cndoom configuration file (chocolate-doom.cfg or cndoom.cfg) had a parameter named video_driver that was left blank (intentionally, to let SDL automatically detect the appropriate driver). However, SDL still fails to find a driver when left to its own volition.
The solution, as it would seem, was to provide the video_driver parameter in the configuration file with windib as the value (inside the provided quotation marks). directx as the value, however, would cause SDL to fail.
So, where does the issue lie? I honestly do not know, but there are a list of suspects:
1) cndoom is mucking around in the environment variables and overwriting them, then failing to handle a rare contingency (see I_InitGraphics function in src/i_video.c). The thing that speaks against this is that directx as video_mode value causes the program to crash. Maybe cndoom does not expect directx to fail?
2) SDL 1.2 can't properly detect DirectX on some newer machines. Then SDL fails to default back to windib if directx fails.
3) Windows 8.1 does not set up environment variables for its drivers in rare cases. There might also be a change in environment variable naming policy?
4) User error. A likely case, although a case I have no information about.
If I could detect the source of the error I could file a bug report, but seeing as I did not experience the issue first-hand that makes matters difficult.
If the application you are trying to run fails in a similar manner to cndoom, but no video_driver parameter (or equivalent) is available in any configuration file I believe the issue can be resolved if you have access to the source code. Simply make a call to SDL_setenv and provide the appropriate values before (using this as reference) the call to SDL_Init and compile.
TL;DR
If an SDL 1.2 application crashes silently (or giving a 'No available video device' error), try finding a configuration file and entering an appropriate value for the video driver parameter (for Windows I believe windib is as failsafe as you can get). If no such configuration file or parameter exists you will need access to the source code and make a call to SDL_setenv (with an appropriate value for the video_driver parameter) before SDL_Init.
Sunday, April 13, 2014
Writing a Very, Very Simple Emulator
In this post I will be covering an interesting topic that I myself came in contact with a few years ago now; Writing an emulator. For the purposes of simplicity and learning, I will present a very minimal architecture that we will model in software, but the principles learned here will be applicable to existing hardware.
Common for most architectures is the presence of various forms of state tracking mechanisms. Random access memory is one such mechanism. Here an application can store data that can be fetched and manipulated to progress or revert the state of the application. Another state tracking mechanism that is usually less known unless you are interested in hardware design or have been working with low-level languages are the so-called processor registers. Many architectures can only perform operations on registers, meaning data has to be loaded from RAM to a register, manipulated, and then stored back into RAM. Some registers on a processor has a specific purpose, and is not available for general purpose computing. Two such registers are the stack pointer which points to the top value of the stack, and the instruction pointer which points to current instruction that is being executed by the processor.
For the purposes of this tutorial we will be emulating a 8-bit CPU architecture called CRAP, which is an acronym for something. Use your imagination. Being an 8-bit architecture entails that the processor can only distinguish between 256 unique memory locations. On our architecture the size of each location is 8 bits. CRAP has an instruction pointer, but no stack pointer or any general purpose registers, since it can work directly on memory in RAM. All CRAP machines are expected to have the full amount of RAM available to function properly.
Each instruction is three bytes wide. The first byte is the opcode. It tells the processor what to do (addition, subtraction, multiplication, stack push, etc). The second byte will be the destination operand (let's call it A), while the third byte will be the source operand (called B), so that a complete instruction will result in something similar; A = A + B should the opcode be addition. All operations are unsigned.
Both code and program storage share the 256 bytes of RAM. Code is stored in reverse from address 255 down to zero. The programmer is free to store program state in any memory location since we don't have any stack pointer. However, the programmer must be certain not to store program state where code is stored, lest the program become corrupted, or the programmer actually intends to modify the code at run-time.
The instruction set contains the following opcodes where A and B are the operands:
0 Does nothing.
1 Returns control to parent process (terminate execution).
2 Sets value in address A to value in address B.
3 Sets value in address A to value B.
4 Adds value in address A and value in address B and stores result in address A.
5 Subtracts value in address A and value in address B and stores result in address A.
6 Multiplies value in address A and value in address B and stores result in address A.
7 Divides value in address A and value in address B and stores result in address A.
8 Ands value in address A and value in address B and stores result in address A.
9 Ors value in address A and value in address B and stores result in address A.
10 Xors value in address A and value in address B and stores result in address A.
11 Shifts value in address A by value in address B to the right and stores result in address A.
12 Shifts value in address A by value in address B to the left and stores result in address A.
13 Skips one instruction if value in address A is less than value in address B.
14 Skips one instruction if value in address A is greater than value in address B.
15 Skips one instruction if value in address A is equal to value in address B.
16 Sets instruction pointer to value in address A.
17 Prints a string of values starting from address A with the size of value in address B.
18 Prints a string of characters starting from address A with the size of value in address B.*
What does a machine like this look like in code. Here's one implementation.
class CRAP
{
private:
unsigned char I; // instruction pointer
unsigned char RAM[256]; // random access memory
public:
void SetProgram(const std::list<unsigned char> &program);
void Run( void );
};
First, we need to load a program into RAM. This is what the SetProgram function is for. Remember that the program is loaded into memory in reverse beginning from address 255.
void CRAP::SetProgram(const std::list<unsigned char> &program)
{
std::list<unsigned char>::const_iterator instr = program.begin();
for (unsigned char i = 0; i < (unsigned char)program.size(); ++i, ++ instr) {
RAM[255-i] = *instr;
}
RAM[255 - program.size()] = 1; // safe-guard in case there is no program-return
}
Second, we need to implement the Run() function. This is the meat of the emulator. Basically, this function will initially set the instruction pointer to 255. This is the starting point for all programs. The program will then read three bytes from RAM in reverse using the instruction pointer and then perform an operation depending on . This process will repeat until the operation byte has the value 1 at which point the function will terminate.
There are two ways of writing the main execution loop; Using either a switch-case-centric design, or function table. According to my tests a switch-case is about four times faster than the function table, so we'll go with that.
void CRAP::Run( void )
{
I = 255; // initialize program counter
while (true) {
unsigned char O = RAM[I--]; // operation
unsigned char A = RAM[I--]; // first operand
unsigned char B = RAM[I--]; // second operand
switch (O) {
case 0: break;
case 1: return;
case 2: RAM[A] = RAM[B]; break;
case 3: RAM[A] = B; break;
case 4: RAM[A] += RAM[B]; break;
case 5: RAM[A] -= RAM[B]; break;
case 6: RAM[A] *= RAM[B]; break;
case 7: RAM[A] /= RAM[B]; break;
case 8: RAM[A] &= RAM[B]; break;
case 9: RAM[A] |= RAM[B]; break;
case 10: RAM[A] ^= RAM[B]; break;
case 11: RAM[A] >>= RAM[B]; break;
case 12: RAM[A] <<= RAM[B]; break;
case 13: if (RAM[A] < RAM[B]) { I -= 3; } break;
case 14: if (RAM[A] > RAM[B]) { I -= 3; }; break;
case 15: if (RAM[A] == RAM[B]) { I -= 3; }; break;
case 16: I = RAM[A]; break;
case 17: for (unsigned int i = 0; i < RAM[B]; ++i) { std::cout << (unsigned int)RAM[(unsigned char)(A+i)]; } break;
case 18: for (unsigned int i = 0; i < RAM[B]; ++i) { std::cout << RAM[(unsigned char)(A+i)]; } break;
default: std::cout << "Illegal instruction"; return;
}
}
}
I would also like to mention that you programmers that have ever implemented a calculator have already implemented the core ideas that make an emulator work. Take a look at this.
It's the exact same thing that an emulator does! It reads the operation and the operands, and performs an action on those operands based on the operation. The major difference is that an emulator may or may not have more complex operations, operations that involve memory addresses rather than constant values, and state preservation, but hey, an advanced calculator can do that as well!
// Format: A + B (where + can be substituted for -, *, or /)
void ProcessOperation(const std::vector<std::string> &input)
{
// input: Let's say the input is a user string that has
// been split up into individual words by a parser
if (input.size() != 3 || input[1].size() != 1) {
std::cout << “Invalid formatting.” << std::endl;
return;
}
int A = atoi(input[0]);
char instruction = input[1][0];
int B = atoi(input[2]);
switch (instruction) {
case '+': std::cout << “ans=” << A + B << std::endl; break;
case '-': std::cout << “ans=” << A - B << std::endl; break;
case '*': std::cout << “ans=” << A * B << std::endl; break;
case '/': std::cout << “ans=” << A / B << std::endl; break;
default: std::cout << “Invalid operation” << std::endl;
}
}
It's the exact same thing that an emulator does! It reads the operation and the operands, and performs an action on those operands based on the operation. The major difference is that an emulator may or may not have more complex operations, operations that involve memory addresses rather than constant values, and state preservation, but hey, an advanced calculator can do that as well!
Now we really only need one more component to complete our emulator. Need is somewhat of a strong word. We already have a working emulator. However, it would be nice and convenient to actually write a program externally and load it into memory so we don't need to hard-code each program and recompile the entire emulator to run it. I'll keep this as simple as possible:
bool LoadProgram(const char *filename, std::list<unsigned char> &out)
{
out.clear();
std::ifstream fin(filename);
if (!fin.is_open()) {
std::cout << "Could not read file" << std::endl;
return false;
}
unsigned int op;
while (fin >> op) {
out.push_back((unsigned char)op);
}
const float instructionCount = float(out.size()) / 3.0f;
if (instructionCount != int(instructionCount)) {
std::cout << "Incomplete instruction" << std::endl;
out.clear();
return false;
}
if (out.size() > 255) {
std::cout << "Program too large" << std::endl;
out.clear();
return false;
}
return true;
}
This function reads plain text files rather than binary files. That makes reading and writing programs a bit easier, but not as easy as if we were to write an assembler that builds programs from human-readable text like a modern assembler. I'll leave implementing that as an exercise for the reader.
To test out the emulator create a new file with the following contents and load it using the LoadProgram function. See if you can decipher what it does before you run it.
3 0 72
3 1 101
3 2 108
3 3 108
3 4 111
3 5 44
3 6 32
3 7 119
3 8 111
3 9 114
3 10 108
3 11 100
3 12 10
3 13 13
18 0 13
3 0 67
3 1 111
3 2 117
3 3 110
3 4 116
3 5 32
3 6 116
3 7 111
3 8 32
3 9 49
3 10 48
3 11 48
3 12 58
3 13 10
3 14 14
18 0 14
3 0 1
3 1 1
3 2 10
3 3 100
3 4 147
17 0 1
18 2 1
4 0 1
14 0 3
16 4 0
1 0 0
Knowing what you know now, I will leave writing an emulator for the CHIP-8 architecture using only the specs for that system as an exercise. By reading only the spec and avoiding looking at the slew of other CHIP-8 emulators that exist, you will know what it is like to write an emulator for a system that has yet to be fully understood by the emulation community.
* Note that the spec is ambiguous in this matter. It says nothing about what value corresponds to what character. A common mistake would be to just assume that the host system character table is the one we should use, but that is not necessarily true. Furthermore, the host system character table might not be the same across all platforms, meaning there will definitely be times where values do not correspond to the correct characters. However, we lack information on the matter, and so we must accept using the host character table for now. This is a perfect example of ambiguities that are faced when emulating actual hardware.
Subscribe to:
Posts (Atom)