Skip to content

Instantly share code, notes, and snippets.

@VladSez
Last active July 7, 2022 15:35
Show Gist options
  • Save VladSez/a0241e69fc056f5ae813f5e1566a46a7 to your computer and use it in GitHub Desktop.
Save VladSez/a0241e69fc056f5ae813f5e1566a46a7 to your computer and use it in GitHub Desktop.
Learn how computers work by simulating them in Javascript. Original: https://github.com/jsdf/little-virtual-computer
// @flow
/*
Components: (do a ctrl-f find for them)
1.MEMORY
2.CPU
3.DISPLAY
4.INPUT
5.AUDIO
6.ASSEMBLER
7.SIMULATION CONTROL
8.BUILT-IN PROGRAMS
*/
// 1.MEMORY
const Memory = {
/*
We are going to use an array to simulate computer memory. We can store a number
value at each position in the array, and we will use a number value to access
each slot in the array (we'll call these array indexes 'memory addresses').
Real computers have memory which can be read and written as individual bytes,
and also in larger or smaller chunks. In real computers memory addresses and
values are usually shown as hexadecimal (base-16) form, due to the fact that
hexadecimal is a concise alternative to binary, which 'lines up' nicely with
binary: a 1 digit hexadecimal number can represent exactly all of the values
which a 4 digit binary number can. However, we are going to represent addresses
and values as base-10 numbers (the kind you're used to), so there's one less
thing you need know about at this point. If you like you can read more about
binary and hexidecimal numbers here (but it's not essential):
https://jamesfriend.com.au/how-do-binary-and-hexadecimal-numbers-work
*/
ram: [],
/*
Here we have the total amount of array slots (or memory addresses) we are
going to have at which to store data values.
The program code will also be loaded into these slots, and when the CPU starts
running, it will begin reading each instruction of the program from memory and
executing it. At the hardware level, program code is just another form of data
stored in memory.
We'll use the first 1000 (0 - 999) slots as working space for our code to use.
The next 1000 (1000 - 1999) we'll load our program code into, and that's where
it will be executed from.
The final 1000 slots will be used to communicate with the input and output (I/O)
devices.
2000 - 2003: the keycode of a key which is currently pressed, from most recently
to least recently started
2010, 2011: the x and y position of the mouse within the screen.
2012: the address of the pixel the mouse is currently on
2013: mouse button status (0 = up, 1 = down)
2050: a random number which changes before every instruction
2051 - 2099: unused
2100 - 2999: The content of the screen, specifically the color values of each
of the pixels of the 30x30 pixel screen, row by row, from the top left.
For example, the top row uses slots 2100 - 2129, and the bottom row uses
slots 2970 - 3000.
3000 - 3008: Memory addresses used to control 3 channels of audio output. This
computer is too simple to play recorded sounds, but can simple tones, which you
can control by setting the addresses for 'wavetype', frequency and volume of
each channel.
*/
TOTAL_MEMORY_SIZE: 3100,
WORKING_MEMORY_START: 0,
WORKING_MEMORY_END: 1000,
PROGRAM_MEMORY_START: 1000,
PROGRAM_MEMORY_END: 2000,
KEYCODE_0_ADDRESS: 2000,
KEYCODE_1_ADDRESS: 2001,
KEYCODE_2_ADDRESS: 2002,
MOUSE_X_ADDRESS: 2010,
MOUSE_Y_ADDRESS: 2011,
MOUSE_PIXEL_ADDRESS: 2012,
MOUSE_BUTTON_ADDRESS: 2013,
RANDOM_NUMBER_ADDRESS: 2050,
CURRENT_TIME_ADDRESS: 2051,
VIDEO_MEMORY_START: 2100,
VIDEO_MEMORY_END: 3000,
AUDIO_CH1_WAVETYPE_ADDRESS: 3000,
AUDIO_CH1_FREQUENCY_ADDRESS: 3001,
AUDIO_CH1_VOLUME_ADDRESS: 3002,
AUDIO_CH2_WAVETYPE_ADDRESS: 3003,
AUDIO_CH2_FREQUENCY_ADDRESS: 3004,
AUDIO_CH2_VOLUME_ADDRESS: 3005,
AUDIO_CH3_WAVETYPE_ADDRESS: 3006,
AUDIO_CH3_FREQUENCY_ADDRESS: 3007,
AUDIO_CH3_VOLUME_ADDRESS: 3008,
// The program will be loaded into the region of memory starting at this slot.
PROGRAM_START: 1000,
// Store a value at a certain address in memory
set(address, value) {
if (isNaN(value)) {
throw new Error(`tried to write to an invalid value at ${address}`);
}
if (address < 0 || address >= this.TOTAL_MEMORY_SIZE) {
throw new Error('tried to write to an invalid memory address');
}
this.ram[address] = value;
},
// Get the value which is stored at a certain address in memory
get(address) {
if (address < 0 || address >= this.TOTAL_MEMORY_SIZE) {
throw new Error('tried to read from an invalid memory address');
}
return this.ram[address];
},
};
// 2.CPU
const CPU = {
/*
These instructions represent the things the CPU can be told to do. We
implement them here with code, but a real CPU would have circuitry
implementing each one of these possible actions, which include things like
loading data from memory, comparing it, operating on and combining it, and
storing it back into Memory.
We assign numerical values called 'opcodes' to each of the instructions. When
our program is 'assembled' from the program code text, the version of the
program that we actually load into memory will use these numeric codes to refer
to the CPU instructions in place of the textual names as a numeric value is a
more efficient representation, especially as computers only directly understand
numbers, whereas text is an abstraction on top of number values.
We'll make the opcodes numbers starting at 9000 to make the values a bit more
distinctive when we see them in the memory viewer. We'll include some extra info
about each of the instructions so our simulator user interface can show it
alongside the 'disassembled' view of the program code in Memory.
There are a lot of these, so it's probably not worth reading the code for each one,
but they are grouped into sections of related instructions, so it might be worth
taking a look at a few in each section. When you're done you can skip ahead to the
next part which defines the 'programCounter'.
*/
instructions: {
// First, some instructions for copying values between places in memory.
// this instruction is typically called 'mov', short for 'move', as in 'move
// value at *this* address to *that* address', but this naming can be a bit
// confusing, because the operation doesn't remove the value at the source
// address, as 'move' might seem to imply, so for clarity we'll call it 'copy_to_from' instead.
copy_to_from: {
opcode: 9000,
description: 'set value at address to the value at the given address',
operands: [['destination', 'address'], ['source', 'address']],
execute(destination, sourceAddress) {
const sourceValue = Memory.get(sourceAddress);
Memory.set(destination, sourceValue);
},
},
copy_to_from_constant: {
opcode: 9001,
description: 'set value at address to the given constant value',
operands: [['destination', 'address'], ['source', 'constant']],
execute(address, sourceValue) {
Memory.set(address, sourceValue);
},
},
copy_to_from_ptr: {
opcode: 9002,
description: `set value at destination address to the value at the
address pointed to by the value at 'source' address`,
operands: [['destination', 'address'], ['source', 'pointer']],
execute(destinationAddress, sourcePointer) {
const sourceAddress = Memory.get(sourcePointer);
const sourceValue = Memory.get(sourceAddress);
Memory.set(destinationAddress, sourceValue);
},
},
copy_into_ptr_from: {
opcode: 9003,
description: `set value at the address pointed to by the value at
'destination' address to the value at the source address`,
operands: [['destination', 'pointer'], ['source', 'address']],
execute(destinationPointer, sourceAddress) {
const destinationAddress = Memory.get(destinationPointer);
const sourceValue = Memory.get(sourceAddress);
Memory.set(destinationAddress, sourceValue);
},
},
copy_address_of_label: {
opcode: 9004,
description: `set value at destination address to the address of the label
given`,
operands: [['destination', 'address'], ['source', 'label']],
execute(destinationAddress, labelAddress) {
Memory.set(destinationAddress, labelAddress);
},
},
// Next, some instructions for performing arithmetic
add: {
opcode: 9010,
description: `add the value at the 'a' address with the value at the 'b'
address and store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'address'], ['result', 'address']],
execute(aAddress, bAddress, resultAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
const result = a + b;
Memory.set(resultAddress, result);
},
},
add_constant: {
opcode: 9011,
description: `add the value at the 'a' address with the constant value 'b' and store
the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']],
execute(aAddress, b, resultAddress) {
const a = Memory.get(aAddress);
const result = a + b;
Memory.set(resultAddress, result);
},
},
subtract: {
opcode: 9020,
description: `from the value at the 'a' address, subtract the value at the
'b' address and store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'address'], ['result', 'address']],
execute(aAddress, bAddress, resultAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
const result = a - b;
Memory.set(resultAddress, result);
},
},
subtract_constant: {
opcode: 9021,
description: `from the value at the 'a' address, subtract the constant value 'b' and
store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']],
execute(aAddress, b, resultAddress) {
const a = Memory.get(aAddress);
const result = a - b;
Memory.set(resultAddress, result);
},
},
multiply: {
opcode: 9030,
description: `multiply the value at the 'a' address and the value at the 'b'
address and store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'address'], ['result', 'address']],
execute(aAddress, bAddress, resultAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
const result = a * b;
Memory.set(resultAddress, result);
},
},
multiply_constant: {
opcode: 9031,
description: `multiply the value at the 'a' address and the constant value 'b' and
store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']],
execute(aAddress, b, resultAddress) {
const a = Memory.get(aAddress);
const result = a * b;
Memory.set(resultAddress, result);
},
},
divide: {
opcode: 9040,
description: `integer divide the value at the 'a' address by the value at
the 'b' address and store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'address'], ['result', 'address']],
execute(aAddress, bAddress, resultAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
if (b === 0) throw new Error('tried to divide by zero');
const result = Math.floor(a / b);
Memory.set(resultAddress, result);
},
},
divide_constant: {
opcode: 9041,
description: `integer divide the value at the 'a' address by the constant value 'b'
and store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']],
execute(aAddress, b, resultAddress) {
const a = Memory.get(aAddress);
if (b === 0) throw new Error('tried to divide by zero');
const result = Math.floor(a / b);
Memory.set(resultAddress, result);
},
},
modulo: {
opcode: 9050,
description: `get the value at the 'a' address modulo the value at the 'b'
address and store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'address'], ['result', 'address']],
execute(aAddress, bAddress, resultAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
if (b === 0) throw new Error('tried to modulo by zero');
const result = a % b;
Memory.set(resultAddress, result);
},
},
modulo_constant: {
opcode: 9051,
description: `get the value at the 'a' address modulo the constant value 'b' and
store the result at the 'result' address`,
operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']],
execute(aAddress, b, resultAddress) {
const a = Memory.get(aAddress);
const result = a % b;
if (b === 0) throw new Error('tried to modulo by zero');
Memory.set(resultAddress, result);
},
},
// some instructions for comparing values
compare: {
opcode: 9090,
description: `compare the value at the 'a' address and the value at the 'b'
address and store the result (-1 for a < b, 0 for a == b, 1 for a > b) at the
'result' address`,
operands: [['a', 'address'], ['b', 'address'], ['result', 'address']],
execute(aAddress, bAddress, resultAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
let result = 0;
if (a < b) {
result = -1;
} else if (a > b) {
result = 1;
}
Memory.set(resultAddress, result);
},
},
compare_constant: {
opcode: 9091,
description: `compare the value at the 'a' address and the constant value
'b' and store the result (-1 for a < b, 0 for a == b, 1 for a > b) at the
'result' address`,
operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']],
execute(aAddress, b, resultAddress) {
const a = Memory.get(aAddress);
let result = 0;
if (a < b) {
result = -1;
} else if (a > b) {
result = 1;
}
Memory.set(resultAddress, result);
},
},
// some instructions for controlling the flow of the program
'jump_to': {
opcode: 9100,
description: `set the program counter to the address of the label specified,
so the program continues from there`,
operands: [['destination', 'label']],
execute(labelAddress) {
CPU.programCounter = labelAddress;
},
},
'branch_if_equal': {
opcode: 9101,
description: `if the value at address 'a' is equal to the value at address
'b', set the program counter to the address of the label specified, so the
program continues from there`,
operands: [['a', 'address'], ['b', 'address'], ['destination', 'label']],
execute(aAddress, bAddress, labelAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
if (a === b) {
CPU.programCounter = labelAddress;
}
},
},
'branch_if_equal_constant': {
opcode: 9102,
description: `if the value at address 'a' is equal to the constant value 'b', set the
program counter to the address of the label specified, so the program continues
from there`,
operands: [['a', 'address'], ['b', 'constant'], ['destination', 'label']],
execute(aAddress, b, labelAddress) {
const a = Memory.get(aAddress);
if (a === b) {
CPU.programCounter = labelAddress;
}
},
},
'branch_if_not_equal': {
opcode: 9103,
description: `if the value at address 'a' is not equal to the value at
address 'b', set the program counter to the address of the label specified, so
the program continues from there`,
operands: [['a', 'address'], ['b', 'address'], ['destination', 'label']],
execute(aAddress, bAddress, labelAddress) {
const a = Memory.get(aAddress);
const b = Memory.get(bAddress);
if (a !== b) {
CPU.programCounter = labelAddress;
}
},
},
'branch_if_not_equal_constant': {
opcode: 9104,
description: `if the value at address 'a' is not equal to the constant value 'b', set
the program counter to the address of the label specified, so the program
continues from there`,
operands: [['a', 'address'], ['b', 'constant'], ['destination', 'label']],
execute(aAddress, b, labelAddress) {
const a = Memory.get(aAddress);
if (a !== b) {
CPU.programCounter = labelAddress;
}
},
},
// some additional miscellanous instructions
'data': {
opcode: 9200,
description: `operands given will be included in the program when it is
compiled at the position that they appear in the code, so you can use a label to
get the address of the data and access it`,
operands: [],
execute() {
},
},
'break': {
opcode: 9998,
description: 'pause program execution, so it must be resumed via simulator UI',
operands: [],
execute() {
CPU.running = false;
},
},
'halt': {
opcode: 9999,
description: 'end program execution, requiring the simulator to be reset to start again',
operands: [],
execute() {
CPU.running = false;
CPU.halted = true;
},
},
},
/*
In a real computer, there are small pieces of memory inside the CPU called
'registers', which just hold one value at a time, but can be accessed
very quickly. These are used for a few different purposes, such as holding a
value that we are going to do some arithmetic operations with, before storing
it back to the main memory of the computer. For simplicity in this simulator
our CPU will just directly with the values in main memory instead.
However, there is one CPU register we do need to simulate: the 'program counter'.
As we move through our program, we need to keep track of where we are up to.
The program counter contains a memory address pointing to the location of the
program instruction we are currently executing.
*/
programCounter: Memory.PROGRAM_START,
/*
We also need to keep track of whether the CPU is running or not. The 'break'
instruction, which is like 'debugger' in Javascript, will be implemented by
setting this to false. This will cause the simulator to stop, but we can still
resume the program
The 'halt' instruction will tell the CPU that we are at the end of the program,
so it should stop executing instructions, and can't be resumed.
*/
running: false,
halted: false,
reset() {
this.programCounter = Memory.PROGRAM_START;
this.halted = false;
this.running = false;
},
/*
Move the program counter forward to the next memory address and return the
opcode or data at that location
*/
advanceProgramCounter() {
if (this.programCounter < Memory.PROGRAM_MEMORY_START || this.programCounter >= Memory.PROGRAM_MEMORY_END) {
throw new Error(`program counter outside valid program memory region at ${this.programCounter}`);
}
return Memory.get(this.programCounter++);
},
/*
We'll set up a mapping between our instruction names and the numerical values
we will turn them into when we assemble the program. It is these numerical
values ('opcodes') which will be interpreted by our simulated CPU as it runs the
program.
*/
instructionsToOpcodes: new Map(),
opcodesToInstructions: new Map(),
/*
Advances through the program by one instruction, getting input from the input
devices (keyboard, mouse), and then executing the instruction. After calling this,
we'll still need to handle writing output to the output devices (screen, audio).
*/
step() {
Input.updateInputs();
const opcode = this.advanceProgramCounter();
const instructionName = this.opcodesToInstructions.get(opcode);
if (!instructionName) {
throw new Error(`Unknown opcode '${opcode}'`);
}
// read as many values from memory as the instruction takes as operands and
// execute the instruction with those operands
const operands = this.instructions[instructionName].operands.map(() =>
this.advanceProgramCounter()
);
this.instructions[instructionName].execute.apply(null, operands);
},
init() {
// Init mapping between our instruction names and opcodes
Object.keys(this.instructions).forEach((instructionName, index) => {
const opcode = this.instructions[instructionName].opcode;
this.instructionsToOpcodes.set(instructionName, opcode);
this.opcodesToInstructions.set(opcode, instructionName);
});
},
};
// 3.DISPLAY
const Display = {
SCREEN_WIDTH: 30,
SCREEN_HEIGHT: 30,
SCREEN_PIXEL_SCALE: 20,
/*
To reduce the amount of memory required to contain the data for each pixel on
the screen, we're going to use a lookup table mapping color IDs to RGB colors.
This is sometimes called a 'color palette'.
This means that rather than having to store a red, green and blue value for each
color, in our simulated program we can just use the ID of the color we want to
use for each pixel, and when the simulated video hardware draws the screen it
can look up the actual RGB color values to use for each pixel rendered.
The drawback of approach is that the colors you can use are much more limited,
as you can only use a color if it's in the palette. It also means you can't
simply lighten or darken colors using math (unless you use a clever layout of
your palette).
*/
COLOR_PALETTE: {
'0': [ 0, 0, 0], // Black
'1': [255,255,255], // White
'2': [255, 0, 0], // Red
'3': [ 0,255, 0], // Lime
'4': [ 0, 0,255], // Blue
'5': [255,255, 0], // Yellow
'6': [ 0,255,255], // Cyan/Aqua
'7': [255, 0,255], // Magenta/Fuchsia
'8': [192,192,192], // Silver
'9': [128,128,128], // Gray
'10': [128, 0, 0], // Maroon
'11': [128,128, 0], // Olive
'12': [ 0,128, 0], // Green
'13': [128, 0,128], // Purple
'14': [ 0,128,128], // Teal
'15': [ 0, 0,128], // Navy
},
getColor(pixelColorId, address) {
const color = this.COLOR_PALETTE[pixelColorId];
if (!color) {
throw new Error(`Invalid color code ${pixelColorId} at address ${address}`);
}
return color;
},
imageData: (null/*: ?ImageData */),
canvasCtx: (null/*: ?CanvasRenderingContext2D */),
/*
Read the pixel values from video memory, look them up in our color palette, and
convert them to the format which the Canvas 2D API requires: an array of RGBA
values for each pixel. This format uses 4 consecutive array slots to represent
each pixel, one for each of the RGBA channels (red, green, blue, alpha).
We don't need to vary the alpha (opacity) values, so we'll just set them to 255
(full opacity) for every pixel.
*/
drawScreen() {
const imageData = notNull(this.imageData);
const videoMemoryLength = Memory.VIDEO_MEMORY_END - Memory.VIDEO_MEMORY_START;
const pixelsRGBA = imageData.data;
for (var i = 0; i < videoMemoryLength; i++) {
const pixelColorId = Memory.ram[Memory.VIDEO_MEMORY_START + i];
const colorRGB = this.getColor(pixelColorId || 0, Memory.VIDEO_MEMORY_START + i);
pixelsRGBA[i * 4] = colorRGB[0];
pixelsRGBA[i * 4 + 1] = colorRGB[1];
pixelsRGBA[i * 4 + 2] = colorRGB[2];
pixelsRGBA[i * 4 + 3] = 255; // full opacity
}
const canvasCtx = notNull(this.canvasCtx);
canvasCtx.putImageData(imageData, 0, 0);
},
init() {
const canvasCtx = notNull(SimulatorUI.getCanvas().getContext('2d'));
this.canvasCtx = canvasCtx;
this.imageData = canvasCtx.createImageData(Display.SCREEN_WIDTH, Display.SCREEN_HEIGHT);
},
};
// 4.INPUT
/*
We make mouse and keyboard input available to our simulated computer by setting
certain locations in memory the current keyboard and mouse states before each
CPU operation.
Because the browser provides an event-based API for input, we need to listen for
relevent keyboard and mouse events and keep track of their state and expose it
to the simulated computer.
*/
const Input = {
keysPressed: new Set(),
mouseDown: false,
mouseX: 0,
mouseY: 0,
init() {
if (!document.body) throw new Error('DOM not ready');
document.body.onkeydown = (event) => {
this.keysPressed.add(event.which);
};
document.body.onkeyup = (event) => {
this.keysPressed.delete(event.which);
};
document.body.onmousedown = () => {
this.mouseDown = true;
};
document.body.onmouseup = () => {
this.mouseDown = false;
};
const screenPageY = SimulatorUI.getCanvas().getBoundingClientRect().top + window.scrollY;
const screenPageX = SimulatorUI.getCanvas().getBoundingClientRect().left + window.scrollX;
SimulatorUI.getCanvas().onmousemove = (event) => {
this.mouseX = Math.floor((event.pageX - screenPageX) / Display.SCREEN_PIXEL_SCALE);
this.mouseY = Math.floor((event.pageY - screenPageY) / Display.SCREEN_PIXEL_SCALE);
};
},
updateInputs() {
const mostRecentKeys = Array.from(this.keysPressed.values()).reverse();
Memory.ram[Memory.KEYCODE_0_ADDRESS] = mostRecentKeys[0] || 0;
Memory.ram[Memory.KEYCODE_1_ADDRESS] = mostRecentKeys[1] || 0;
Memory.ram[Memory.KEYCODE_2_ADDRESS] = mostRecentKeys[2] || 0;
Memory.ram[Memory.MOUSE_BUTTON_ADDRESS] = this.mouseDown ? 1 : 0;
Memory.ram[Memory.MOUSE_X_ADDRESS] = this.mouseX;
Memory.ram[Memory.MOUSE_Y_ADDRESS] = this.mouseY;
Memory.ram[Memory.MOUSE_PIXEL_ADDRESS] = Memory.VIDEO_MEMORY_START + (Math.floor(this.mouseY)) * Display.SCREEN_WIDTH + Math.floor(this.mouseX);
Memory.ram[Memory.RANDOM_NUMBER_ADDRESS] = Math.floor(Math.random() * 255);
Memory.ram[Memory.CURRENT_TIME_ADDRESS] = Date.now();
},
};
// 5.AUDIO
const AudioContext =
window.AudioContext || // Default
window.webkitAudioContext; // Safari and old versions of Chrome
const Audio = {
WAVETYPES: {
'0': 'square',
'1': 'sawtooth',
'2': 'triangle',
'3': 'sine',
},
MAX_GAIN: 0.15,
audioCtx: new AudioContext(),
audioChannels: [],
addAudioChannel(wavetypeAddr, freqAddr, volAddr) {
const oscillatorNode = this.audioCtx.createOscillator();
const gainNode = this.audioCtx.createGain();
oscillatorNode.connect(gainNode);
gainNode.connect(this.audioCtx.destination);
const state = {
gain: 0,
oscillatorType: 'square',
frequency: 440,
};
gainNode.gain.value = state.gain;
oscillatorNode.type = state.oscillatorType;
oscillatorNode.frequency.value = state.frequency;
oscillatorNode.start();
return this.audioChannels.push({
state,
wavetypeAddr,
freqAddr,
volAddr,
gainNode,
oscillatorNode,
});
},
updateAudio() {
this.audioChannels.forEach(channel => {
const frequency = (Memory.ram[channel.freqAddr] || 0) / 1000;
const gain = !CPU.running ? 0 : (Memory.ram[channel.volAddr] || 0) / 100 * this.MAX_GAIN;
const oscillatorType = this.WAVETYPES[Memory.ram[channel.wavetypeAddr] || 0];
const {state} = channel;
if (state.gain !== gain) {
channel.gainNode.gain.setValueAtTime(gain, this.audioCtx.currentTime);
state.gain = gain;
}
if (state.oscillatorType !== oscillatorType) {
channel.oscillatorNode.type = oscillatorType;
state.oscillatorType = oscillatorType;
}
if (state.frequency !== frequency) {
channel.oscillatorNode.frequency.setValueAtTime(frequency, this.audioCtx.currentTime);
state.frequency = frequency;
}
});
},
init() {
this.addAudioChannel(
Memory.AUDIO_CH1_WAVETYPE_ADDRESS,
Memory.AUDIO_CH1_FREQUENCY_ADDRESS,
Memory.AUDIO_CH1_VOLUME_ADDRESS
);
this.addAudioChannel(
Memory.AUDIO_CH2_WAVETYPE_ADDRESS,
Memory.AUDIO_CH2_FREQUENCY_ADDRESS,
Memory.AUDIO_CH2_VOLUME_ADDRESS
);
this.addAudioChannel(
Memory.AUDIO_CH3_WAVETYPE_ADDRESS,
Memory.AUDIO_CH3_FREQUENCY_ADDRESS,
Memory.AUDIO_CH3_VOLUME_ADDRESS
);
},
};
// 6.ASSEMBLER
/*
We use a simple text-based language to input our program. This is our 'assembly
language'. We need to convert it into a form which is made up of only numerical
values so we can load it into our computer's Memory. This is a two step process:
1. parse program text into an array of objects representing our instructions and
their operands.
2. convert the objects into numeric values to be interpreted by the CPU. This is
our 'machine code'.
We parse the program text into tokens by splitting the text into lines, then
splitting those lines into tokens (words), which gives us to an instruction name
and operands for that instruction, from each line.
*/
const Assembler = {
// we'll keep a map of instructions which take a label as an operand so we
// know when to substitute an operand for the corresponding label address
instructionsLabelOperands: new Map(),
initInstructionsLabelOperands() {
Object.keys(CPU.instructions).forEach(name => {
const labelOperandIndex = CPU.instructions[name].operands.findIndex(operand =>
operand[1] === 'label'
);
if (labelOperandIndex > -1) {
this.instructionsLabelOperands.set(name, labelOperandIndex);
}
});
},
// we break our program code into lines, then break those lines into 'tokens',
// and then 'parse' that line of tokens into an instruction plus its operands
parseProgramText(programText) {
const programInstructions = [];
const lines = programText.split('\n');
let line, i;
try {
for (i = 0; i < lines.length; i++) {
line = lines[i];
const instruction = {name: '', operands: []};
let tokens = line.replace(/;.*$/, '') // strip comments
.split(' ');
for (let token of tokens) {
// skip empty tokens
if (token == null || token == "") {
continue;
}
// first token
if (!instruction.name) {
// special case for labels
if (token.endsWith(':')) {
instruction.name = 'label';
instruction.operands.push(token.slice(0, token.length - 1));
break;
}
instruction.name = token; // instruction name token
} else {
// handle text operands
if (
(
// define name
instruction.name === 'define' &&
instruction.operands.length === 0
) || (
// label used as operand
this.instructionsLabelOperands.get(instruction.name) === instruction.operands.length
)
) {
instruction.operands.push(token);
continue;
}
// try to parse number operands
const number = parseInt(token, 10);
if (Number.isNaN(number)) {
instruction.operands.push(token);
} else {
instruction.operands.push(number);
}
}
}
// validate number of operands given
if (
instruction.name &&
instruction.name !== 'label' &&
instruction.name !== 'data' &&
instruction.name !== 'define'
) {
const expectedOperands = CPU.instructions[instruction.name].operands;
if (instruction.operands.length !== expectedOperands.length) {
const error = new Error(
`Wrong number of operands for instruction ${instruction.name}
got ${instruction.operands.length}, expected ${expectedOperands.length}
at line ${i+1}: '${line}'`
);
error.isException = true;
throw error;
}
}
// if instruction was found on this line, add it to the program
if (instruction.name) {
programInstructions.push(instruction);
}
}
} catch (err) {
if (err.isException) throw err; // validation error
// otherwise it must be a parsing/syntax error
throw new Error(`Syntax error on program line ${i+1}: '${line}'`);
}
programInstructions.push({name: 'halt', operands: []});
return programInstructions;
},
/*
Having parsed our program text into an array of objects containing instruction
name and the operands to the instruction, we need to turn those objects into
numeric values we can store in the computer's memory, and load them in there.
*/
assembleAndLoadProgram(programInstructions) {
// 'label' is a special case – it's not really an instruction which the CPU
// understands. Instead, it's a marker for the location of the next
// instruction, which we can substitute for the actual location once we know
// the memory locations in the assembled program which the labels refer to.
const labelAddresses = {};
let labelAddress = Memory.PROGRAM_START;
for (let instruction of programInstructions) {
if (instruction.name === 'label') {
const labelName = instruction.operands[0];
labelAddresses[labelName] = labelAddress;
} else if (instruction.name === 'define') {
continue;
} else {
// advance labelAddress by the length of the instruction and its operands
labelAddress += 1 + instruction.operands.length;
}
}
const defines = {};
// load instructions and operands into memory
let loadingAddress = Memory.PROGRAM_START;
for (let instruction of programInstructions) {
if (instruction.name === 'label') {
continue;
}
if (instruction.name === 'define') {
defines[instruction.operands[0]] = instruction.operands[1];
continue;
}
if (instruction.name === 'data') {
for (var i = 0; i < instruction.operands.length; i++) {
Memory.ram[loadingAddress++] = instruction.operands[i];
}
continue;
}
// for each instruction, we first write the relevant opcode to memory
const opcode = CPU.instructionsToOpcodes.get(instruction.name);
if (!opcode) {
throw new Error(`No opcode found for instruction '${instruction.name}'`);
}
Memory.ram[loadingAddress++] = opcode;
// then, we write the operands for instruction to memory
const operands = instruction.operands.slice(0);
// replace labels used as operands with actual memory address
if (this.instructionsLabelOperands.has(instruction.name)) {
const labelOperandIndex = this.instructionsLabelOperands.get(instruction.name);
if (typeof labelOperandIndex !== 'number') throw new Error('expected number');
const labelName = instruction.operands[labelOperandIndex];
const labelAddress = labelAddresses[labelName];
if (!labelAddress) {
throw new Error(`unknown label '${labelName}'`);
}
operands[labelOperandIndex] = labelAddress;
}
for (var i = 0; i < operands.length; i++) {
let value = null;
if (typeof operands[i] === 'string') {
if (operands[i] in defines) {
value = defines[operands[i]];
} else {
throw new Error(`'${operands[i]}' not defined`);
}
} else {
value = operands[i];
}
Memory.ram[loadingAddress++] = value;
}
}
},
init() {
this.initInstructionsLabelOperands();
}
};
// 7.SIMULATION CONTROL
const Simulation = {
CYCLES_PER_YIELD: 997,
delayBetweenCycles: 0,
loop() {
if (Simulation.delayBetweenCycles === 0) {
// running full speed, execute a bunch of instructions before yielding
// to the JS event loop, to achieve decent 'real time' execution speed
for (var i = 0; i < Simulation.CYCLES_PER_YIELD; i++) {
if (!CPU.running) {
Simulation.stop();
break;
}
CPU.step();
}
} else {
// run only one execution before yielding to the JS event loop so screen
// and UI changes can be shown, and new mouse and keyboard input taken
CPU.step();
SimulatorUI.updateUI();
}
Simulation.updateOutputs();
if (CPU.running) {
setTimeout(Simulation.loop, Simulation.delayBetweenCycles);
}
},
run() {
CPU.running = true;
SimulatorUI.updateUI();
SimulatorUI.updateSpeedUI();
this.loop();
},
stop() {
CPU.running = false;
SimulatorUI.updateUI();
SimulatorUI.updateSpeedUI();
},
updateOutputs() {
Display.drawScreen();
Audio.updateAudio();
},
loadProgramAndReset() {
/*
In a real computer, memory addresses which have never had any value set are
considered 'uninitialized', and might contain any garbage value, but to keep
our simulation simple we're going to initialize every location with the value
0. However, just like in a real computer, in our simulation it is possible
for us to mistakenly read from the wrong place in memory if we have a bug in
our simulated program where we get the memory address wrong.
*/
for (var i = 0; i < Memory.TOTAL_MEMORY_SIZE; i++) {
Memory.ram[i] = 0;
}
const programText = SimulatorUI.getProgramText();
try {
Assembler.assembleAndLoadProgram(Assembler.parseProgramText(programText));
} catch (err) {
alert(err.message);
console.error(err);
}
SimulatorUI.setLoadedProgramText(programText);
CPU.reset();
this.updateOutputs();
SimulatorUI.updateProgramMemoryView();
SimulatorUI.updateUI();
SimulatorUI.updateSpeedUI();
},
stepOnce() {
CPU.running = true;
CPU.step();
CPU.running = false;
this.updateOutputs();
SimulatorUI.updateUI();
},
runStop() {
if (CPU.running) {
this.stop();
} else {
this.run();
}
},
}
// 8.BUILT-IN PROGRAMS
const PROGRAMS = {
'Add':
`
define a 0
define b 1
define result 2
copy_to_from_constant a 4
copy_to_from_constant b 4
add a b result
; look at memory location 2, you should now see '8'
`,
'RandomPixels':
`
define videoStartAddr 2100
define videoEndAddr 3000
define randomNumberAddr 2050
define numColors 16
FillScreen:
define fillScreenPtr 0 ; address at which store address of current screen pixel in loop
copy_to_from_constant fillScreenPtr videoStartAddr ; initialize to point to first pixel
jump_to FillScreenLoop
FillScreenLoop:
define tempAddr 1 ; address to use for temporary storage
; modulo random value by number of colors in palette to get a random color...
modulo_constant randomNumberAddr numColors tempAddr
; ...and write it to current screen pixel, eg. the address pointed to by fillScreenPtr
copy_into_ptr_from fillScreenPtr tempAddr
; increment pointer to point to next screen pixel address
add_constant fillScreenPtr 1 fillScreenPtr
branch_if_not_equal_constant fillScreenPtr videoEndAddr FillScreenLoop ; if not finished, repeat
jump_to FillScreen ; filled screen, now start again from the top
`,
'Paint':
`Init:
define colorPickerStartAddr 2100
define colorPickerEndAddr 2116
define mousePixelAddr 2012
define mouseButtonAddr 2013
define currentColorAddr 0
define loopCounterAddr 1
define numColors 16
define comparisonResultAddr 4
define lastClickedAddr 2
define lessThanResult -1
copy_to_from_constant loopCounterAddr colorPickerStartAddr ; init loop counter to start of video memory
copy_to_from_constant currentColorAddr 0 ; we'll use this while drawing color picker
DrawColorPickerLoop:
copy_into_ptr_from loopCounterAddr currentColorAddr
add_constant loopCounterAddr 1 loopCounterAddr
add_constant currentColorAddr 1 currentColorAddr
branch_if_not_equal_constant loopCounterAddr colorPickerEndAddr DrawColorPickerLoop
copy_to_from_constant currentColorAddr 3; initial color (green)
MainLoop:
branch_if_equal_constant mouseButtonAddr loopCounterAddr HandleClick
jump_to MainLoop
HandleClick:
copy_to_from lastClickedAddr mousePixelAddr ; store mouse location in case it changes
compare_constant lastClickedAddr colorPickerEndAddr comparisonResultAddr
branch_if_equal_constant comparisonResultAddr lessThanResult SelectColor
jump_to PaintAtCursor
SelectColor:
subtract_constant lastClickedAddr colorPickerStartAddr currentColorAddr
jump_to MainLoop
PaintAtCursor:
copy_into_ptr_from lastClickedAddr currentColorAddr ; set pixel at mouse cursor to color at currentColorAddr
jump_to MainLoop
`,
'ChocolateRain': `
define accumulatorAddr 0
define dataTempAddr 1
define musicPlayheadPtr 2
define startTimeAddr 3
define channelDestinationPtr 4
define currentTimeAddr 2051
define beatLengthInMS 200
define ch1WaveTypeAddr 3000
define ch1FreqAddr 3001
define ch2WaveTypeAddr 3003
copy_to_from_constant ch1WaveTypeAddr 3 ; sine
copy_to_from_constant ch2WaveTypeAddr 0 ; sawtooth
Reset:
copy_to_from startTimeAddr currentTimeAddr ; keep time started to calculate time elapsed
copy_address_of_label musicPlayheadPtr MusicData
WaitForEvent:
; calculate current beat from time
subtract currentTimeAddr startTimeAddr accumulatorAddr
divide_constant accumulatorAddr beatLengthInMS accumulatorAddr
copy_to_from_ptr dataTempAddr musicPlayheadPtr
branch_if_equal_constant dataTempAddr -1 Reset
compare accumulatorAddr dataTempAddr dataTempAddr
branch_if_not_equal_constant dataTempAddr -1 PlayNote
jump_to WaitForEvent
PlayNote:
; advance source pointer to channel data
add_constant musicPlayheadPtr 1 musicPlayheadPtr
; move dest pointer to frequency address for channel
copy_to_from_constant channelDestinationPtr ch1FreqAddr ; move to ch1FreqAddr
; in dataTempAddr, calculate relative offset of channel's frequency address from ch1FreqAddr
copy_to_from_ptr dataTempAddr musicPlayheadPtr
multiply_constant dataTempAddr 3 dataTempAddr
; increment pointer by channel offset to point to correct channel's frequency address
add channelDestinationPtr dataTempAddr channelDestinationPtr
add_constant musicPlayheadPtr 1 musicPlayheadPtr ; advance source pointer to frequency data
; copy frequency
copy_to_from_ptr dataTempAddr musicPlayheadPtr
copy_into_ptr_from channelDestinationPtr dataTempAddr
; move destination pointer to volume address for channel
add_constant channelDestinationPtr 1 channelDestinationPtr
; advance source pointer to volume dataTempAddr
add_constant musicPlayheadPtr 1 musicPlayheadPtr
; copy volume
copy_to_from_ptr dataTempAddr musicPlayheadPtr
copy_into_ptr_from channelDestinationPtr dataTempAddr
add_constant musicPlayheadPtr 1 musicPlayheadPtr ; advance to next music event
jump_to WaitForEvent
MusicData:
data 0 1 195997 53
data 0 0 622253 53
data 0 0 130812 53
data 1 0 622253 0
data 1 0 622253 58
data 2 1 195997 0
data 2 1 155563 56
data 2 0 130812 0
data 2 0 622253 0
data 2 0 783990 68
data 3 0 783990 0
data 3 0 523251 49
data 4 1 155563 0
data 4 1 195997 64
data 4 0 523251 0
data 4 0 698456 64
data 4 0 233081 52
data 5 0 698456 0
data 5 0 466163 50
data 6 1 195997 0
data 6 0 233081 0
data 6 0 466163 0
data 6 0 587329 64
data 7 0 587329 0
data 7 0 622253 60
data 7 0 195997 51
data 8 0 622253 0
data 8 0 311126 43
data 9 0 195997 0
data 9 0 311126 0
data 9 0 523251 69
data 10 0 523251 0
data 10 0 391995 50
data 11 0 391995 0
data 11 0 587329 71
data 11 0 146832 50
data 12 0 146832 0
data 12 0 587329 0
data 12 0 391995 50
data 13 0 391995 0
data 13 0 466163 61
data 14 0 466163 0
data 14 0 523251 63
data 14 0 155563 50
data 15 1 146832 50
data 15 0 523251 0
data 15 0 391995 51
data 16 1 146832 0
data 16 1 155563 57
data 16 0 155563 0
data 16 0 391995 0
data 16 0 622253 68
data 16 0 207652 60
data 17 0 622253 0
data 17 0 622253 60
data 18 1 155563 0
data 18 1 195997 62
data 18 0 207652 0
data 18 0 622253 0
data 18 0 783990 63
data 18 0 311126 70
data 19 1 195997 0
data 19 1 174614 54
data 19 0 783990 0
data 19 0 523251 46
data 20 1 174614 0
data 20 0 311126 0
data 20 0 523251 0
data 20 0 698456 66
data 20 0 293664 57
data 21 1 146832 55
data 21 0 698456 0
data 21 0 466163 51
data 22 0 293664 0
data 22 0 466163 0
data 22 0 587329 65
data 22 0 233081 52
data 23 1 146832 0
data 23 1 155563 59
data 23 0 233081 0
data 23 0 587329 0
data 23 0 622253 63
data 23 0 261625 65
data 24 0 622253 0
data 24 0 311126 41
data 25 1 155563 0
data 25 1 130812 57
data 25 0 261625 0
data 25 0 311126 0
data 25 0 523251 66
data 25 0 195997 58
data 26 0 523251 0
data 26 0 391995 53
data 27 1 130812 0
data 27 1 146832 60
data 27 0 195997 0
data 27 0 391995 0
data 27 0 587329 69
data 27 0 233081 63
data 28 0 587329 0
data 28 0 391995 52
data 29 1 146832 0
data 29 1 116540 56
data 29 0 233081 0
data 29 0 391995 0
data 29 0 466163 59
data 30 1 116540 0
data 30 1 130812 63
data 30 0 466163 0
data 30 0 523251 61
data 30 0 261625 56
data 31 0 523251 0
data 31 0 391995 50
data 32 1 130812 0
data 32 1 233081 65
data 32 0 261625 0
data 32 0 391995 0
data 32 0 622253 73
data 32 0 207652 53
data 33 0 622253 0
data 33 0 622253 60
data 34 1 233081 0
data 34 1 155563 52
data 34 0 207652 0
data 34 0 622253 0
data 34 0 783990 64
data 35 0 783990 0
data 35 0 523251 50
data 36 1 155563 0
data 36 1 195997 62
data 36 0 523251 0
data 36 0 932327 71
data 36 0 174614 53
data 37 0 932327 0
data 37 0 466163 43
data 38 1 195997 0
data 38 0 174614 0
data 38 0 466163 0
data 38 0 587329 62
data 39 0 587329 0
data 39 0 622253 60
data 39 0 261625 50
data 40 0 622253 0
data 40 0 311126 43
data 41 0 261625 0
data 41 0 311126 0
data 41 0 523251 66
data 42 0 523251 0
data 42 0 391995 53
data 43 0 391995 0
data 43 0 587329 68
data 43 0 293664 55
data 44 0 293664 0
data 44 0 587329 0
data 44 0 391995 49
data 45 0 391995 0
data 45 0 466163 67
data 46 0 466163 0
data 46 0 523251 67
data 46 0 311126 50
data 47 1 146832 54
data 47 0 523251 0
data 47 0 523251 61
data 48 1 146832 0
data 48 1 155563 60
data 48 0 311126 0
data 48 0 523251 0
data 48 0 1046502 71
data 48 0 207652 53
data 49 0 1046502 0
data 49 0 523251 45
data 50 1 155563 0
data 50 1 195997 64
data 50 0 207652 0
data 50 0 523251 0
data 50 0 783990 68
data 51 1 195997 0
data 51 1 174614 60
data 51 0 783990 0
data 51 0 783990 58
data 52 1 174614 0
data 52 0 783990 0
data 52 0 932327 64
data 52 0 195997 53
data 53 1 146832 50
data 53 0 932327 0
data 53 0 466163 43
data 54 0 195997 0
data 54 0 466163 0
data 54 0 698456 64
data 55 1 146832 0
data 55 1 155563 58
data 55 0 698456 0
data 55 0 783990 64
data 55 0 207652 51
data 56 0 783990 0
data 56 0 415304 43
data 57 1 155563 0
data 57 1 130812 56
data 57 0 207652 0
data 57 0 415304 0
data 57 0 622253 67
data 58 0 622253 0
data 58 0 523251 56
data 59 1 130812 0
data 59 1 146832 57
data 59 0 523251 0
data 59 0 698456 71
data 59 0 233081 57
data 60 0 233081 0
data 60 0 698456 0
data 60 0 466163 49
data 61 1 146832 0
data 61 1 116540 52
data 61 0 466163 0
data 61 0 587329 64
data 62 1 116540 0
data 62 1 130812 57
data 62 0 587329 0
data 62 0 622253 62
data 62 0 261625 56
data 64 1 130812 0
data 64 1 195997 64
data 64 0 261625 0
data 64 0 622253 0
data 64 0 622253 61
data 64 0 130812 52
data -1
`,
'Custom 1': '',
'Custom 2': '',
'Custom 3': '',
};
// boring code for rendering user interface of the simulator
// not really important for understanding how computers work
const UI = {
$(selector) {
const el = document.querySelector(selector);
if (el == null) throw new Error(`couldn't find selector '${selector}'`);
return el;
},
$Input(selector) {
const el = UI.$(selector);
if (el instanceof HTMLInputElement) return el;
throw new Error('expected HTMLInputElement');
},
$TextArea(selector) {
const el = UI.$(selector);
if (el instanceof HTMLTextAreaElement) return el;
throw new Error('expected HTMLTextAreaElement');
},
$Button(selector) {
const el = UI.$(selector);
if (el instanceof HTMLButtonElement) return el;
throw new Error('expected HTMLButtonElement');
},
$Canvas(selector) {
const el = UI.$(selector);
if (el instanceof HTMLCanvasElement) return el;
throw new Error('expected HTMLCanvasElement');
},
$Select(selector) {
const el = UI.$(selector);
if (el instanceof HTMLSelectElement) return el;
throw new Error('expected HTMLSelectElement');
},
virtualizedScrollView(container, containerHeight, itemHeight, numItems, renderItems) {
Object.assign(container.style, {
height: `${containerHeight}px`,
overflow: 'auto',
});
const content = document.createElement('div');
Object.assign(content.style, {
height: `${itemHeight * numItems}px`,
overflow: 'hidden',
});
container.appendChild(content);
const rows = document.createElement('div');
content.appendChild(rows);
const overscan = 10; // how many rows above/below viewport to render
const renderRowsInView = () => requestAnimationFrame(() => {
const start = Math.max(0, Math.floor(container.scrollTop / itemHeight) - overscan);
const end = Math.min(numItems, Math.ceil((container.scrollTop + containerHeight) / itemHeight) + overscan);
const offsetTop = start * itemHeight;
rows.style.transform = `translateY(${offsetTop}px)`;
rows.innerHTML = renderItems(start, end);
});
container.onscroll = renderRowsInView;
return renderRowsInView;
}
};
const SimulatorUI = {
selectedProgram: localStorage.getItem('selectedProgram') || 'RandomPixels',
initUI() {
const programSelectorEl = UI.$Select('#programSelector');
// init program selector
Object.keys(PROGRAMS).forEach(programName => {
const option = document.createElement('option');
option.value = programName;
option.textContent = programName;
programSelectorEl.append(option);
});
programSelectorEl.value = this.selectedProgram;
this.selectProgram();
},
getProgramText() {
return UI.$TextArea('#program').value;
},
getCanvas() {
return UI.$Canvas('#canvas');
},
initScreen(width, height, pixelScale) {
let imageRendering = 'pixelated';
if (/firefox/i.test(navigator.userAgent)) {
imageRendering = '-moz-crisp-edges';
}
Object.assign(SimulatorUI.getCanvas(), {width, height});
// scale our (very low resolution) canvas up to a more viewable size using CSS transforms
// $FlowFixMe: ignore unknown property '-ms-interpolation-mode'
Object.assign(SimulatorUI.getCanvas().style, {
transformOrigin: 'top left',
transform: `scale(${pixelScale})`,
'-ms-interpolation-mode': 'nearest-neighbor',
imageRendering,
});
},
loadedProgramText: '',
setLoadedProgramText(programText) {
this.loadedProgramText = programText;
UI.$Button('#loadProgramButton').disabled = true;
},
updateLoadProgramButton() {
UI.$Button('#loadProgramButton').disabled = this.loadedProgramText === this.getProgramText();
},
selectProgram() {
this.selectedProgram = UI.$Select('#programSelector').value;
localStorage.setItem('selectedProgram', this.selectedProgram);
UI.$TextArea('#program').value =
localStorage.getItem(this.selectedProgram) || PROGRAMS[this.selectedProgram] || '';
this.updateLoadProgramButton();
},
editProgramText() {
if (this.selectedProgram.startsWith('Custom')) {
localStorage.setItem(this.selectedProgram, UI.$TextArea('#program').value);
}
this.updateLoadProgramButton();
},
setSpeed() {
Simulation.delayBetweenCycles = -parseInt(UI.$Input('#speed').value, 10);
this.updateSpeedUI();
},
setFullspeed() {
const fullspeedEl = UI.$Input('#fullspeed');
if (fullspeedEl && fullspeedEl.checked) {
Simulation.delayBetweenCycles = 0;
} else {
Simulation.delayBetweenCycles = 1;
}
this.updateSpeedUI();
},
updateSpeedUI() {
const fullspeed = Simulation.delayBetweenCycles === 0;
const runningAtFullspeed = CPU.running && fullspeed;
UI.$Input('#fullspeed').checked = fullspeed;
UI.$Input('#speed').value = String(-Simulation.delayBetweenCycles);
UI.$('#debugger').classList.toggle('fullspeed', runningAtFullspeed);
UI.$('#debuggerMessageArea').textContent = runningAtFullspeed ?
'debug UI disabled when CPU.running at full speed' : '';
},
updateUI() {
UI.$Input('#programCounter').value = String(CPU.programCounter);
if (CPU.halted) {
UI.$('#running').textContent = 'halted';
UI.$Button('#stepButton').disabled = true;
UI.$Button('#runButton').disabled = true;
} else {
UI.$('#running').textContent = CPU.running ? 'running' : 'paused';
UI.$Button('#stepButton').disabled = false;
UI.$Button('#runButton').disabled = false;
}
this.updateWorkingMemoryView();
this.updateInputMemoryView();
this.updateVideoMemoryView();
this.updateAudioMemoryView();
if (Simulation.delayBetweenCycles > 300 || !CPU.running) {
if (typeof this.scrollToProgramLine == 'function') {
this.scrollToProgramLine(Math.max(0, CPU.programCounter - Memory.PROGRAM_MEMORY_START - 3));
}
}
},
updateWorkingMemoryView() {
const lines = [];
for (var i = Memory.WORKING_MEMORY_START; i < Memory.WORKING_MEMORY_END; i++) {
lines.push(`${i}: ${Memory.ram[i]}`);
}
UI.$TextArea('#workingMemoryView').textContent = lines.join('\n');
},
scrollToProgramLine: (item) => {},
updateProgramMemoryView() {
const lines = [];
for (var i = Memory.PROGRAM_MEMORY_START; i < Memory.PROGRAM_MEMORY_END; i++) {
const instruction = CPU.opcodesToInstructions.get(Memory.ram[i]);
lines.push(`${padRight(i, 4)}: ${padRight(Memory.ram[i], 8)} ${instruction || ''}`);
if (instruction) {
const operands = CPU.instructions[instruction].operands;
for (var j = 0; j < operands.length; j++) {
lines.push(`${padRight(i + 1 + j, 4)}: ${padRight(Memory.ram[i + 1 + j], 8)} ${operands[j][0]} (${operands[j][1]})`);
}
i += operands.length;
}
}
const itemHeight = 14;
const renderProgramMemoryView = UI.virtualizedScrollView(
UI.$('#programMemoryView'),
136,
itemHeight,
lines.length,
(start, end) => (
lines.slice(start, end)
.map((l, i) => {
const current = Memory.PROGRAM_MEMORY_START + start + i === CPU.programCounter;
return `
<pre
class="tablerow"
style="height: ${itemHeight}px; background: ${current ? '#eee' : 'none'}"
>${l}</pre>
`;
})
.join('')
)
);
this.scrollToProgramLine = (item) => {
UI.$('#programMemoryView').scrollTop = item * itemHeight;
renderProgramMemoryView();
};
renderProgramMemoryView();
},
updateInputMemoryView() {
UI.$TextArea('#inputMemoryView').textContent =
`${Memory.KEYCODE_0_ADDRESS}: ${padRight(Memory.ram[Memory.KEYCODE_0_ADDRESS], 8)} keycode 0
${Memory.KEYCODE_1_ADDRESS}: ${padRight(Memory.ram[Memory.KEYCODE_1_ADDRESS], 8)} keycode 1
${Memory.KEYCODE_2_ADDRESS}: ${padRight(Memory.ram[Memory.KEYCODE_2_ADDRESS], 8)} keycode 2
${Memory.MOUSE_X_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_X_ADDRESS], 8)} mouse x
${Memory.MOUSE_Y_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_Y_ADDRESS], 8)} mouse y
${Memory.MOUSE_PIXEL_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_PIXEL_ADDRESS], 8)} mouse pixel
${Memory.MOUSE_BUTTON_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_BUTTON_ADDRESS], 8)} mouse button
${Memory.RANDOM_NUMBER_ADDRESS}: ${padRight(Memory.ram[Memory.RANDOM_NUMBER_ADDRESS], 8)} random number
${Memory.CURRENT_TIME_ADDRESS}: ${padRight(Memory.ram[Memory.CURRENT_TIME_ADDRESS], 8)} current time`;
},
updateVideoMemoryView() {
const lines = [];
for (var i = Memory.VIDEO_MEMORY_START; i < Memory.VIDEO_MEMORY_END; i++) {
lines.push(`${i}: ${Memory.ram[i]}`);
}
UI.$TextArea('#videoMemoryView').textContent = lines.join('\n');
},
updateAudioMemoryView() {
UI.$TextArea('#audioMemoryView').textContent =
`${Memory.AUDIO_CH1_WAVETYPE_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH1_WAVETYPE_ADDRESS], 8)} audio ch1 wavetype
${Memory.AUDIO_CH1_FREQUENCY_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH1_FREQUENCY_ADDRESS], 8)} audio ch1 frequency
${Memory.AUDIO_CH1_VOLUME_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH1_VOLUME_ADDRESS], 8)} audio ch1 volume
${Memory.AUDIO_CH2_WAVETYPE_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH2_WAVETYPE_ADDRESS], 8)} audio ch2 wavetype
${Memory.AUDIO_CH2_FREQUENCY_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH2_FREQUENCY_ADDRESS], 8)} audio ch2 frequency
${Memory.AUDIO_CH2_VOLUME_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH2_VOLUME_ADDRESS], 8)} audio ch2 volume
${Memory.AUDIO_CH3_WAVETYPE_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH3_WAVETYPE_ADDRESS], 8)} audio ch3 wavetype
${Memory.AUDIO_CH3_FREQUENCY_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH3_FREQUENCY_ADDRESS], 8)} audio ch3 frequency
${Memory.AUDIO_CH3_VOLUME_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH3_VOLUME_ADDRESS], 8)} audio ch3 volume`;
},
}
function clamp(val, min, max) {
return Math.min(min, Math.max(max, val));
}
function padRight(input, length) {
const str = input + '';
let padded = str;
for (var i = str.length; i < length; i++) {
padded += " ";
}
return padded;
}
/*:: declare function notNull<T>(val: ?T): T; */
function notNull(val) {
if (val != null) return val;
throw new Error('unexpected null');
}
CPU.init();
Display.init();
Input.init();
Audio.init();
Assembler.init();
SimulatorUI.initScreen(Display.SCREEN_WIDTH, Display.SCREEN_HEIGHT, Display.SCREEN_PIXEL_SCALE);
SimulatorUI.initUI();
Simulation.loadProgramAndReset();
// enable audio to work with chrome autoplay policy :'(
if (!document.body) throw new Error('DOM not ready');
function resumeAudio() {
if (!document.body) throw new Error('DOM not ready');
document.body.removeEventListener('click', resumeAudio);
Audio.audioCtx.resume()
}
document.body.addEventListener('click', resumeAudio);
/*
binary-and-hexadecimal.md
Before explaining how computers load data into their working space and process it, it's valuable to understand binary and hexadecimal numbers. This is because computer hardware only understands binary values due to the physical characteristics of the electronic circuitry used to implement them. I won't go further into explaining the reasons why computer hardware works with values in binary form, but you can read more about it [here](http://nookkin.com/articles/computer-science/why-computers-use-binary.ndoc).
So what is binary? Binary is a 'base-2 number system'. But what does that mean?
Consider the number system we are all accustomed to using in our everyday lives, which is sometimes called the decimal system or base-10. It uses the digits 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9 to represent the first 10 integers (whole-numbers) starting from zero. It is called base-10 because we have 10 digits to work with, from 0 to 9. But what happens after 9? With the number 10 we move over one column to the left, placing a '1' in the 'tens column' followed by a '0' in the 'ones column'. If we continue to increase our number in increments of 1, the digit in the 'ones column' moves through the digits 0-9, until we get to 20, and so on until we eventually get to 100, placing a 1 in the 'hundreds column'.
Binary, or base-2, is much the same, except the only digits we have to work with are 0 and 1. Then how do we count? It's actually the same as in base-10, but after 0, then 1, we get to 10. Why? Because we have moved through all the digits we have to work with in the 'ones column', so we put a 1 in the next column to the left. However, in binary, that column is not the 'tens column', but rather the 'twos column'. In the same way that in the decimal number 20 we are basically saying that we have 'two tens and zero ones', in the binary number 10 we are saying that we have 'one twos and zero ones'. Next comes 11 (one twos and one ones) then 100 (one fours, zero twos, and zero ones).
0
1
10
11
100
101
If it seems confusing that the columns, from right to left are 'ones', 'two', 'fours', rather than 'ones', 'tens', 'hundreds' consider that in base-10 we only need a tens column once we've exhausted all of the digits we can put in the ones column (0-9) once we reach the number 9, and the next whole number after 9 is 10, but in base-2 we only have 0 and 1, so after 0, then 1, we have exhausted all the digits for the ones column, and the next number we want to represent is the number that (in base-10) we would call 'two'. By calling it the 'twos column' we're still using the base-10 name for that number. It's valuable to understand that each number can be represented in both base-2 and base-10, or any other base for that matter, and the only difference is how we write them in digits (or however else we are recording them, such as in the two positions of a switch). As we continue on to larger and larger numbers we have the columns ones, twos, fours, eights, 16s, 32s, 64s and so on. You might recognise these as the powers of 2.
Hexadecimal (base-16) is much like binary and decimal, except that there are 16 digits: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, and f. After 9 we start using letters of the alphabet to fill out the remaining digits to bring us to a total of 16. This means that as we are counting, after 9 we don't go to 10, but instead a, then b, c, e, and finally f, before getting to 10. Instead of the column to the left of the ones column being the tens column, in hexadecimal it is the '16s column'. Once we have moved through 0 to f in that column (moving through 0 to f in the ones column for each digit in the 16s column), the next column is the
256s, 4096s, 65536s, and so on, moving up in the powers of 16. As the columns in binary (base-2) go up in powers of 2 as we move to the left, and the columns in decimal (base-10) in powers of 10, it makes sense that the columns in hexadecimal go up in powers of 16.
hex binary decimal
0 0 0
1 1 1
2 10 2
3 11 3
4 100 4
5 101 5
6 110 6
7 111 7
8 1000 8
9 1001 9
a 1010 10
b 1011 11
c 1100 12
d 1101 13
e 1110 14
f 1111 15
10 1 0000 16
11 1 0001 17
12 1 0010 18
13 1 0011 19
14 1 0100 20
15 1 0101 21
16 1 0110 22
17 1 0111 23
18 1 1000 24
19 1 1001 25
1a 1 1010 26
1b 1 1011 27
1c 1 1100 28
1d 1 1101 29
1e 1 1110 30
1f 1 1111 31
20 10 0000 32
21 10 0001 33
... ... ...
3f 11 1111 63
40 100 0000 64
41 100 0001 65
... ... ...
7f 111 1111 127
80 1000 0000 128
81 1000 0001 129
... ... ...
f8 1111 1000 248
f9 1111 1001 249
fa 1111 1010 250
fb 1111 1011 251
fc 1111 1100 252
fd 1111 1101 253
fe 1111 1110 254
ff 1111 1111 255
100 1 0000 0000 256
101 1 0000 0001 257
If you've ever wondered why power-of-2 numbers like 8, 16, 32, 64, and 256 come up a lot in computer programming, have a look at the binary and hex representations which those decimal values line up with. You'll see that there are 16 values (0-15, because we start counting at zero) which can be represented with (or 'fit inside') 4 binary digits, or 1 hex digit, and 256 values (0-255) which fit inside 8 binary digits/2 hex digits.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment