├── .flowconfig ├── .prettierrc ├── yarn.lock ├── package.json ├── .gitignore ├── index.html ├── README.md ├── computer1.html ├── binary-and-hexadecimal.md └── computer1.js /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | flow-bin@^0.42.0: 6 | version "0.42.0" 7 | resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.42.0.tgz#05dd754b6b052de7b150f9210e2160746961e3cf" 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "little-virtual-computer", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:jsdf/little-virtual-computer.git", 6 | "author": "James Friend ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "flow-bin": "^0.42.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore docs files 2 | _gh_pages 3 | _site 4 | .ruby-version 5 | 6 | # Numerous always-ignore extensions 7 | *.diff 8 | *.err 9 | *.orig 10 | *.log 11 | *.rej 12 | *.swo 13 | *.swp 14 | *.zip 15 | *.vi 16 | *~ 17 | 18 | # OS or Editor folders 19 | .DS_Store 20 | ._* 21 | Thumbs.db 22 | .cache 23 | .project 24 | .settings 25 | .tmproj 26 | *.esproj 27 | nbproject 28 | *.sublime-project 29 | *.sublime-workspace 30 | .idea 31 | 32 | # Komodo 33 | *.komodoproject 34 | .komodotools 35 | 36 | # Folders to ignore 37 | node_modules 38 | bower_components 39 | 40 | lib/ 41 | example/output/ 42 | 43 | main.jsbundle 44 | 45 | images/generated/ -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Little Virtual Computer 4 | 5 |

Little Virtual Computer

6 | 7 |

Have you ever wanted to know how a computer works? How the software is understood by the computer at a hardware level?

8 | 9 |

Let's build a simulated computer in Javascript (because everyone knows Javascript these days), to look at how it all fits together.

10 | 11 |

Start here: computer1.js

12 | 13 |

Then read this you want to know more about binary and hexadecimal numbers (not required to understand the simulated computer, but useful to understand how real computers work).

-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Little Virtual Computer 2 | 3 | Have you ever wanted to know how a computer works? How the software is 4 | understood by the computer at a hardware level? 5 | 6 | Let's build a simulated computer in Javascript (because JS seems to be the Lingua 7 | Franca of code nowadays), to see at how it all fits together. 8 | 9 | You can [try out the simulated computer](https://jsdf.github.io/little-virtual-computer/computer1) and then [read the entire source in one single Javascript file](computer1.js) 10 | 11 | Then [read this](binary-and-hexadecimal.md) you want to know more about binary 12 | and hexadecimal numbers (not required to understand the simulated computer, but 13 | useful to understand how real computers work). 14 | -------------------------------------------------------------------------------- /computer1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Little Virtual Computer 1 5 | 34 | 35 | 36 |
37 |

Little Virtual Computer

38 | This is a simple simulated computer. You can read the source code here. Click 'run/pause' to make it go, or load a different program with the program selector in the bottom left. 39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 | 51 | 53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 |
62 | 63 | 64 |
65 |
66 | 67 | working memory (slots 0 - 999): 68 | 69 | program memory (slots 1000 - 1999): 70 |
71 | input memory (slots 2000 - 2051): 72 | 73 | video memory (slots 2100 - 2999): 74 | 75 | audio memory (slots 3000 - 3000): 76 | 77 |
78 |
79 |
80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /binary-and-hexadecimal.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | So what is binary? Binary is a 'base-2 number system'. But what does that mean? 4 | 5 | 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'. 6 | 7 | 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). 8 | 9 | 0 10 | 1 11 | 10 12 | 11 13 | 100 14 | 101 15 | 16 | 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. 17 | 18 | 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 19 | 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. 20 | 21 | hex binary decimal 22 | 0 0 0 23 | 1 1 1 24 | 2 10 2 25 | 3 11 3 26 | 4 100 4 27 | 5 101 5 28 | 6 110 6 29 | 7 111 7 30 | 8 1000 8 31 | 9 1001 9 32 | a 1010 10 33 | b 1011 11 34 | c 1100 12 35 | d 1101 13 36 | e 1110 14 37 | f 1111 15 38 | 10 1 0000 16 39 | 11 1 0001 17 40 | 12 1 0010 18 41 | 13 1 0011 19 42 | 14 1 0100 20 43 | 15 1 0101 21 44 | 16 1 0110 22 45 | 17 1 0111 23 46 | 18 1 1000 24 47 | 19 1 1001 25 48 | 1a 1 1010 26 49 | 1b 1 1011 27 50 | 1c 1 1100 28 51 | 1d 1 1101 29 52 | 1e 1 1110 30 53 | 1f 1 1111 31 54 | 20 10 0000 32 55 | 21 10 0001 33 56 | ... ... ... 57 | 3f 11 1111 63 58 | 40 100 0000 64 59 | 41 100 0001 65 60 | ... ... ... 61 | 7f 111 1111 127 62 | 80 1000 0000 128 63 | 81 1000 0001 129 64 | ... ... ... 65 | f8 1111 1000 248 66 | f9 1111 1001 249 67 | fa 1111 1010 250 68 | fb 1111 1011 251 69 | fc 1111 1100 252 70 | fd 1111 1101 253 71 | fe 1111 1110 254 72 | ff 1111 1111 255 73 | 100 1 0000 0000 256 74 | 101 1 0000 0001 257 75 | 76 | 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. 77 | -------------------------------------------------------------------------------- /computer1.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | 5 | Components: (do a ctrl-f find for them) 6 | 1.MEMORY 7 | 2.CPU 8 | 3.DISPLAY 9 | 4.INPUT 10 | 5.AUDIO 11 | 6.ASSEMBLER 12 | 7.SIMULATION CONTROL 13 | 8.BUILT-IN PROGRAMS 14 | 15 | */ 16 | 17 | // 1.MEMORY 18 | 19 | const Memory = { 20 | /* 21 | We are going to use an array to simulate computer memory. We can store a number 22 | value at each position in the array, and we will use a number value to access 23 | each slot in the array (we'll call these array indexes 'memory addresses'). 24 | 25 | Real computers have memory which can be read and written as individual bytes, 26 | and also in larger or smaller chunks. In real computers memory addresses and 27 | values are usually shown as hexadecimal (base-16) form, due to the fact that 28 | hexadecimal is a concise alternative to binary, which 'lines up' nicely with 29 | binary: a 1 digit hexadecimal number can represent exactly all of the values 30 | which a 4 digit binary number can. However, we are going to represent addresses 31 | and values as base-10 numbers (the kind you're used to), so there's one less 32 | thing you need know about at this point. If you like you can read more about 33 | binary and hexidecimal numbers here (but it's not essential): 34 | https://jamesfriend.com.au/how-do-binary-and-hexadecimal-numbers-work 35 | */ 36 | ram: [], 37 | 38 | /* 39 | Here we have the total amount of array slots (or memory addresses) we are 40 | going to have at which to store data values. 41 | 42 | The program code will also be loaded into these slots, and when the CPU starts 43 | running, it will begin reading each instruction of the program from memory and 44 | executing it. At the hardware level, program code is just another form of data 45 | stored in memory. 46 | 47 | We'll use the first 1000 (0 - 999) slots as working space for our code to use. 48 | The next 1000 (1000 - 1999) we'll load our program code into, and that's where 49 | it will be executed from. 50 | The final 1000 slots will be used to communicate with the input and output (I/O) 51 | devices. 52 | 2000 - 2003: the keycode of a key which is currently pressed, from most recently 53 | to least recently started 54 | 2010, 2011: the x and y position of the mouse within the screen. 55 | 2012: the address of the pixel the mouse is currently on 56 | 2013: mouse button status (0 = up, 1 = down) 57 | 2050: a random number which changes before every instruction 58 | 2051 - 2099: unused 59 | 2100 - 2999: The content of the screen, specifically the color values of each 60 | of the pixels of the 30x30 pixel screen, row by row, from the top left. 61 | For example, the top row uses slots 2100 - 2129, and the bottom row uses 62 | slots 2970 - 3000. 63 | 3000 - 3008: Memory addresses used to control 3 channels of audio output. This 64 | computer is too simple to play recorded sounds, but can simple tones, which you 65 | can control by setting the addresses for 'wavetype', frequency and volume of 66 | each channel. 67 | */ 68 | TOTAL_MEMORY_SIZE: 3100, 69 | WORKING_MEMORY_START: 0, 70 | WORKING_MEMORY_END: 1000, 71 | PROGRAM_MEMORY_START: 1000, 72 | PROGRAM_MEMORY_END: 2000, 73 | KEYCODE_0_ADDRESS: 2000, 74 | KEYCODE_1_ADDRESS: 2001, 75 | KEYCODE_2_ADDRESS: 2002, 76 | MOUSE_X_ADDRESS: 2010, 77 | MOUSE_Y_ADDRESS: 2011, 78 | MOUSE_PIXEL_ADDRESS: 2012, 79 | MOUSE_BUTTON_ADDRESS: 2013, 80 | RANDOM_NUMBER_ADDRESS: 2050, 81 | CURRENT_TIME_ADDRESS: 2051, 82 | VIDEO_MEMORY_START: 2100, 83 | VIDEO_MEMORY_END: 3000, 84 | AUDIO_CH1_WAVETYPE_ADDRESS: 3000, 85 | AUDIO_CH1_FREQUENCY_ADDRESS: 3001, 86 | AUDIO_CH1_VOLUME_ADDRESS: 3002, 87 | AUDIO_CH2_WAVETYPE_ADDRESS: 3003, 88 | AUDIO_CH2_FREQUENCY_ADDRESS: 3004, 89 | AUDIO_CH2_VOLUME_ADDRESS: 3005, 90 | AUDIO_CH3_WAVETYPE_ADDRESS: 3006, 91 | AUDIO_CH3_FREQUENCY_ADDRESS: 3007, 92 | AUDIO_CH3_VOLUME_ADDRESS: 3008, 93 | 94 | // The program will be loaded into the region of memory starting at this slot. 95 | PROGRAM_START: 1000, 96 | 97 | // Store a value at a certain address in memory 98 | set(address, value) { 99 | if (isNaN(value)) { 100 | throw new Error(`tried to write to an invalid value at ${address}`); 101 | } 102 | if (address < 0 || address >= this.TOTAL_MEMORY_SIZE) { 103 | throw new Error('tried to write to an invalid memory address'); 104 | } 105 | this.ram[address] = value; 106 | }, 107 | 108 | // Get the value which is stored at a certain address in memory 109 | get(address) { 110 | if (address < 0 || address >= this.TOTAL_MEMORY_SIZE) { 111 | throw new Error('tried to read from an invalid memory address'); 112 | } 113 | return this.ram[address]; 114 | }, 115 | }; 116 | 117 | // 2.CPU 118 | 119 | const CPU = { 120 | /* 121 | These instructions represent the things the CPU can be told to do. We 122 | implement them here with code, but a real CPU would have circuitry 123 | implementing each one of these possible actions, which include things like 124 | loading data from memory, comparing it, operating on and combining it, and 125 | storing it back into Memory. 126 | 127 | We assign numerical values called 'opcodes' to each of the instructions. When 128 | our program is 'assembled' from the program code text, the version of the 129 | program that we actually load into memory will use these numeric codes to refer 130 | to the CPU instructions in place of the textual names as a numeric value is a 131 | more efficient representation, especially as computers only directly understand 132 | numbers, whereas text is an abstraction on top of number values. 133 | 134 | We'll make the opcodes numbers starting at 9000 to make the values a bit more 135 | distinctive when we see them in the memory viewer. We'll include some extra info 136 | about each of the instructions so our simulator user interface can show it 137 | alongside the 'disassembled' view of the program code in Memory. 138 | 139 | There are a lot of these, so it's probably not worth reading the code for each one, 140 | but they are grouped into sections of related instructions, so it might be worth 141 | taking a look at a few in each section. When you're done you can skip ahead to the 142 | next part which defines the 'programCounter'. 143 | */ 144 | instructions: { 145 | // First, some instructions for copying values between places in memory. 146 | 147 | // this instruction is typically called 'mov', short for 'move', as in 'move 148 | // value at *this* address to *that* address', but this naming can be a bit 149 | // confusing, because the operation doesn't remove the value at the source 150 | // address, as 'move' might seem to imply, so for clarity we'll call it 'copy_to_from' instead. 151 | copy_to_from: { 152 | opcode: 9000, 153 | description: 'set value at address to the value at the given address', 154 | operands: [['destination', 'address'], ['source', 'address']], 155 | execute(destination, sourceAddress) { 156 | const sourceValue = Memory.get(sourceAddress); 157 | Memory.set(destination, sourceValue); 158 | }, 159 | }, 160 | copy_to_from_constant: { 161 | opcode: 9001, 162 | description: 'set value at address to the given constant value', 163 | operands: [['destination', 'address'], ['source', 'constant']], 164 | execute(address, sourceValue) { 165 | Memory.set(address, sourceValue); 166 | }, 167 | }, 168 | copy_to_from_ptr: { 169 | opcode: 9002, 170 | description: `set value at destination address to the value at the 171 | address pointed to by the value at 'source' address`, 172 | operands: [['destination', 'address'], ['source', 'pointer']], 173 | execute(destinationAddress, sourcePointer) { 174 | const sourceAddress = Memory.get(sourcePointer); 175 | const sourceValue = Memory.get(sourceAddress); 176 | Memory.set(destinationAddress, sourceValue); 177 | }, 178 | }, 179 | copy_into_ptr_from: { 180 | opcode: 9003, 181 | description: `set value at the address pointed to by the value at 182 | 'destination' address to the value at the source address`, 183 | operands: [['destination', 'pointer'], ['source', 'address']], 184 | execute(destinationPointer, sourceAddress) { 185 | const destinationAddress = Memory.get(destinationPointer); 186 | const sourceValue = Memory.get(sourceAddress); 187 | Memory.set(destinationAddress, sourceValue); 188 | }, 189 | }, 190 | copy_address_of_label: { 191 | opcode: 9004, 192 | description: `set value at destination address to the address of the label 193 | given`, 194 | operands: [['destination', 'address'], ['source', 'label']], 195 | execute(destinationAddress, labelAddress) { 196 | Memory.set(destinationAddress, labelAddress); 197 | }, 198 | }, 199 | 200 | // Next, some instructions for performing arithmetic 201 | add: { 202 | opcode: 9010, 203 | description: `add the value at the 'a' address with the value at the 'b' 204 | address and store the result at the 'result' address`, 205 | operands: [['a', 'address'], ['b', 'address'], ['result', 'address']], 206 | execute(aAddress, bAddress, resultAddress) { 207 | const a = Memory.get(aAddress); 208 | const b = Memory.get(bAddress); 209 | const result = a + b; 210 | Memory.set(resultAddress, result); 211 | }, 212 | }, 213 | add_constant: { 214 | opcode: 9011, 215 | description: `add the value at the 'a' address with the constant value 'b' and store 216 | the result at the 'result' address`, 217 | operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']], 218 | execute(aAddress, b, resultAddress) { 219 | const a = Memory.get(aAddress); 220 | const result = a + b; 221 | Memory.set(resultAddress, result); 222 | }, 223 | }, 224 | subtract: { 225 | opcode: 9020, 226 | description: `from the value at the 'a' address, subtract the value at the 227 | 'b' address and store the result at the 'result' address`, 228 | operands: [['a', 'address'], ['b', 'address'], ['result', 'address']], 229 | execute(aAddress, bAddress, resultAddress) { 230 | const a = Memory.get(aAddress); 231 | const b = Memory.get(bAddress); 232 | const result = a - b; 233 | Memory.set(resultAddress, result); 234 | }, 235 | }, 236 | subtract_constant: { 237 | opcode: 9021, 238 | description: `from the value at the 'a' address, subtract the constant value 'b' and 239 | store the result at the 'result' address`, 240 | operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']], 241 | execute(aAddress, b, resultAddress) { 242 | const a = Memory.get(aAddress); 243 | const result = a - b; 244 | Memory.set(resultAddress, result); 245 | }, 246 | }, 247 | multiply: { 248 | opcode: 9030, 249 | description: `multiply the value at the 'a' address and the value at the 'b' 250 | address and store the result at the 'result' address`, 251 | operands: [['a', 'address'], ['b', 'address'], ['result', 'address']], 252 | execute(aAddress, bAddress, resultAddress) { 253 | const a = Memory.get(aAddress); 254 | const b = Memory.get(bAddress); 255 | const result = a * b; 256 | Memory.set(resultAddress, result); 257 | }, 258 | }, 259 | multiply_constant: { 260 | opcode: 9031, 261 | description: `multiply the value at the 'a' address and the constant value 'b' and 262 | store the result at the 'result' address`, 263 | operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']], 264 | execute(aAddress, b, resultAddress) { 265 | const a = Memory.get(aAddress); 266 | const result = a * b; 267 | Memory.set(resultAddress, result); 268 | }, 269 | }, 270 | divide: { 271 | opcode: 9040, 272 | description: `integer divide the value at the 'a' address by the value at 273 | the 'b' address and store the result at the 'result' address`, 274 | operands: [['a', 'address'], ['b', 'address'], ['result', 'address']], 275 | execute(aAddress, bAddress, resultAddress) { 276 | const a = Memory.get(aAddress); 277 | const b = Memory.get(bAddress); 278 | if (b === 0) throw new Error('tried to divide by zero'); 279 | const result = Math.floor(a / b); 280 | Memory.set(resultAddress, result); 281 | }, 282 | }, 283 | divide_constant: { 284 | opcode: 9041, 285 | description: `integer divide the value at the 'a' address by the constant value 'b' 286 | and store the result at the 'result' address`, 287 | operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']], 288 | execute(aAddress, b, resultAddress) { 289 | const a = Memory.get(aAddress); 290 | if (b === 0) throw new Error('tried to divide by zero'); 291 | const result = Math.floor(a / b); 292 | Memory.set(resultAddress, result); 293 | }, 294 | }, 295 | modulo: { 296 | opcode: 9050, 297 | description: `get the value at the 'a' address modulo the value at the 'b' 298 | address and store the result at the 'result' address`, 299 | operands: [['a', 'address'], ['b', 'address'], ['result', 'address']], 300 | execute(aAddress, bAddress, resultAddress) { 301 | const a = Memory.get(aAddress); 302 | const b = Memory.get(bAddress); 303 | if (b === 0) throw new Error('tried to modulo by zero'); 304 | const result = a % b; 305 | Memory.set(resultAddress, result); 306 | }, 307 | }, 308 | modulo_constant: { 309 | opcode: 9051, 310 | description: `get the value at the 'a' address modulo the constant value 'b' and 311 | store the result at the 'result' address`, 312 | operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']], 313 | execute(aAddress, b, resultAddress) { 314 | const a = Memory.get(aAddress); 315 | const result = a % b; 316 | if (b === 0) throw new Error('tried to modulo by zero'); 317 | Memory.set(resultAddress, result); 318 | }, 319 | }, 320 | 321 | // some instructions for comparing values 322 | compare: { 323 | opcode: 9090, 324 | description: `compare the value at the 'a' address and the value at the 'b' 325 | address and store the result (-1 for a < b, 0 for a == b, 1 for a > b) at the 326 | 'result' address`, 327 | operands: [['a', 'address'], ['b', 'address'], ['result', 'address']], 328 | execute(aAddress, bAddress, resultAddress) { 329 | const a = Memory.get(aAddress); 330 | const b = Memory.get(bAddress); 331 | let result = 0; 332 | if (a < b) { 333 | result = -1; 334 | } else if (a > b) { 335 | result = 1; 336 | } 337 | Memory.set(resultAddress, result); 338 | }, 339 | }, 340 | compare_constant: { 341 | opcode: 9091, 342 | description: `compare the value at the 'a' address and the constant value 343 | 'b' and store the result (-1 for a < b, 0 for a == b, 1 for a > b) at the 344 | 'result' address`, 345 | operands: [['a', 'address'], ['b', 'constant'], ['result', 'address']], 346 | execute(aAddress, b, resultAddress) { 347 | const a = Memory.get(aAddress); 348 | let result = 0; 349 | if (a < b) { 350 | result = -1; 351 | } else if (a > b) { 352 | result = 1; 353 | } 354 | Memory.set(resultAddress, result); 355 | }, 356 | }, 357 | 358 | // some instructions for controlling the flow of the program 359 | 'jump_to': { 360 | opcode: 9100, 361 | description: `set the program counter to the address of the label specified, 362 | so the program continues from there`, 363 | operands: [['destination', 'label']], 364 | execute(labelAddress) { 365 | CPU.programCounter = labelAddress; 366 | }, 367 | }, 368 | 'branch_if_equal': { 369 | opcode: 9101, 370 | description: `if the value at address 'a' is equal to the value at address 371 | 'b', set the program counter to the address of the label specified, so the 372 | program continues from there`, 373 | operands: [['a', 'address'], ['b', 'address'], ['destination', 'label']], 374 | execute(aAddress, bAddress, labelAddress) { 375 | const a = Memory.get(aAddress); 376 | const b = Memory.get(bAddress); 377 | if (a === b) { 378 | CPU.programCounter = labelAddress; 379 | } 380 | }, 381 | }, 382 | 'branch_if_equal_constant': { 383 | opcode: 9102, 384 | description: `if the value at address 'a' is equal to the constant value 'b', set the 385 | program counter to the address of the label specified, so the program continues 386 | from there`, 387 | operands: [['a', 'address'], ['b', 'constant'], ['destination', 'label']], 388 | execute(aAddress, b, labelAddress) { 389 | const a = Memory.get(aAddress); 390 | if (a === b) { 391 | CPU.programCounter = labelAddress; 392 | } 393 | }, 394 | }, 395 | 'branch_if_not_equal': { 396 | opcode: 9103, 397 | description: `if the value at address 'a' is not equal to the value at 398 | address 'b', set the program counter to the address of the label specified, so 399 | the program continues from there`, 400 | operands: [['a', 'address'], ['b', 'address'], ['destination', 'label']], 401 | execute(aAddress, bAddress, labelAddress) { 402 | const a = Memory.get(aAddress); 403 | const b = Memory.get(bAddress); 404 | if (a !== b) { 405 | CPU.programCounter = labelAddress; 406 | } 407 | }, 408 | }, 409 | 'branch_if_not_equal_constant': { 410 | opcode: 9104, 411 | description: `if the value at address 'a' is not equal to the constant value 'b', set 412 | the program counter to the address of the label specified, so the program 413 | continues from there`, 414 | operands: [['a', 'address'], ['b', 'constant'], ['destination', 'label']], 415 | execute(aAddress, b, labelAddress) { 416 | const a = Memory.get(aAddress); 417 | if (a !== b) { 418 | CPU.programCounter = labelAddress; 419 | } 420 | }, 421 | }, 422 | 423 | // some additional miscellanous instructions 424 | 'data': { 425 | opcode: 9200, 426 | description: `operands given will be included in the program when it is 427 | compiled at the position that they appear in the code, so you can use a label to 428 | get the address of the data and access it`, 429 | operands: [], 430 | execute() { 431 | }, 432 | }, 433 | 'break': { 434 | opcode: 9998, 435 | description: 'pause program execution, so it must be resumed via simulator UI', 436 | operands: [], 437 | execute() { 438 | CPU.running = false; 439 | }, 440 | }, 441 | 'halt': { 442 | opcode: 9999, 443 | description: 'end program execution, requiring the simulator to be reset to start again', 444 | operands: [], 445 | execute() { 446 | CPU.running = false; 447 | CPU.halted = true; 448 | }, 449 | }, 450 | }, 451 | 452 | /* 453 | In a real computer, there are small pieces of memory inside the CPU called 454 | 'registers', which just hold one value at a time, but can be accessed 455 | very quickly. These are used for a few different purposes, such as holding a 456 | value that we are going to do some arithmetic operations with, before storing 457 | it back to the main memory of the computer. For simplicity in this simulator 458 | our CPU will just directly with the values in main memory instead. 459 | 460 | However, there is one CPU register we do need to simulate: the 'program counter'. 461 | As we move through our program, we need to keep track of where we are up to. 462 | The program counter contains a memory address pointing to the location of the 463 | program instruction we are currently executing. 464 | */ 465 | programCounter: Memory.PROGRAM_START, 466 | 467 | /* 468 | We also need to keep track of whether the CPU is running or not. The 'break' 469 | instruction, which is like 'debugger' in Javascript, will be implemented by 470 | setting this to false. This will cause the simulator to stop, but we can still 471 | resume the program 472 | The 'halt' instruction will tell the CPU that we are at the end of the program, 473 | so it should stop executing instructions, and can't be resumed. 474 | */ 475 | running: false, 476 | halted: false, 477 | 478 | reset() { 479 | this.programCounter = Memory.PROGRAM_START; 480 | this.halted = false; 481 | this.running = false; 482 | }, 483 | 484 | /* 485 | Move the program counter forward to the next memory address and return the 486 | opcode or data at that location 487 | */ 488 | advanceProgramCounter() { 489 | if (this.programCounter < Memory.PROGRAM_MEMORY_START || this.programCounter >= Memory.PROGRAM_MEMORY_END) { 490 | throw new Error(`program counter outside valid program memory region at ${this.programCounter}`); 491 | } 492 | return Memory.get(this.programCounter++); 493 | }, 494 | 495 | /* 496 | We'll set up a mapping between our instruction names and the numerical values 497 | we will turn them into when we assemble the program. It is these numerical 498 | values ('opcodes') which will be interpreted by our simulated CPU as it runs the 499 | program. 500 | */ 501 | instructionsToOpcodes: new Map(), 502 | opcodesToInstructions: new Map(), 503 | 504 | /* 505 | Advances through the program by one instruction, getting input from the input 506 | devices (keyboard, mouse), and then executing the instruction. After calling this, 507 | we'll still need to handle writing output to the output devices (screen, audio). 508 | */ 509 | step() { 510 | Input.updateInputs(); 511 | const opcode = this.advanceProgramCounter(); 512 | const instructionName = this.opcodesToInstructions.get(opcode); 513 | if (!instructionName) { 514 | throw new Error(`Unknown opcode '${opcode}'`); 515 | } 516 | 517 | // read as many values from memory as the instruction takes as operands and 518 | // execute the instruction with those operands 519 | const operands = this.instructions[instructionName].operands.map(() => 520 | this.advanceProgramCounter() 521 | ); 522 | this.instructions[instructionName].execute.apply(null, operands); 523 | }, 524 | 525 | init() { 526 | // Init mapping between our instruction names and opcodes 527 | Object.keys(this.instructions).forEach((instructionName, index) => { 528 | const opcode = this.instructions[instructionName].opcode; 529 | this.instructionsToOpcodes.set(instructionName, opcode); 530 | this.opcodesToInstructions.set(opcode, instructionName); 531 | }); 532 | }, 533 | }; 534 | 535 | // 3.DISPLAY 536 | 537 | const Display = { 538 | SCREEN_WIDTH: 30, 539 | SCREEN_HEIGHT: 30, 540 | SCREEN_PIXEL_SCALE: 20, 541 | 542 | /* 543 | To reduce the amount of memory required to contain the data for each pixel on 544 | the screen, we're going to use a lookup table mapping color IDs to RGB colors. 545 | This is sometimes called a 'color palette'. 546 | 547 | This means that rather than having to store a red, green and blue value for each 548 | color, in our simulated program we can just use the ID of the color we want to 549 | use for each pixel, and when the simulated video hardware draws the screen it 550 | can look up the actual RGB color values to use for each pixel rendered. 551 | 552 | The drawback of approach is that the colors you can use are much more limited, 553 | as you can only use a color if it's in the palette. It also means you can't 554 | simply lighten or darken colors using math (unless you use a clever layout of 555 | your palette). 556 | */ 557 | 558 | COLOR_PALETTE: { 559 | '0': [ 0, 0, 0], // Black 560 | '1': [255,255,255], // White 561 | '2': [255, 0, 0], // Red 562 | '3': [ 0,255, 0], // Lime 563 | '4': [ 0, 0,255], // Blue 564 | '5': [255,255, 0], // Yellow 565 | '6': [ 0,255,255], // Cyan/Aqua 566 | '7': [255, 0,255], // Magenta/Fuchsia 567 | '8': [192,192,192], // Silver 568 | '9': [128,128,128], // Gray 569 | '10': [128, 0, 0], // Maroon 570 | '11': [128,128, 0], // Olive 571 | '12': [ 0,128, 0], // Green 572 | '13': [128, 0,128], // Purple 573 | '14': [ 0,128,128], // Teal 574 | '15': [ 0, 0,128], // Navy 575 | }, 576 | 577 | getColor(pixelColorId, address) { 578 | const color = this.COLOR_PALETTE[pixelColorId]; 579 | if (!color) { 580 | throw new Error(`Invalid color code ${pixelColorId} at address ${address}`); 581 | } 582 | return color; 583 | }, 584 | 585 | imageData: (null/*: ?ImageData */), 586 | canvasCtx: (null/*: ?CanvasRenderingContext2D */), 587 | 588 | /* 589 | Read the pixel values from video memory, look them up in our color palette, and 590 | convert them to the format which the Canvas 2D API requires: an array of RGBA 591 | values for each pixel. This format uses 4 consecutive array slots to represent 592 | each pixel, one for each of the RGBA channels (red, green, blue, alpha). 593 | 594 | We don't need to vary the alpha (opacity) values, so we'll just set them to 255 595 | (full opacity) for every pixel. 596 | */ 597 | drawScreen() { 598 | const imageData = notNull(this.imageData); 599 | const videoMemoryLength = Memory.VIDEO_MEMORY_END - Memory.VIDEO_MEMORY_START; 600 | const pixelsRGBA = imageData.data; 601 | for (var i = 0; i < videoMemoryLength; i++) { 602 | const pixelColorId = Memory.ram[Memory.VIDEO_MEMORY_START + i]; 603 | const colorRGB = this.getColor(pixelColorId || 0, Memory.VIDEO_MEMORY_START + i); 604 | pixelsRGBA[i * 4] = colorRGB[0]; 605 | pixelsRGBA[i * 4 + 1] = colorRGB[1]; 606 | pixelsRGBA[i * 4 + 2] = colorRGB[2]; 607 | pixelsRGBA[i * 4 + 3] = 255; // full opacity 608 | } 609 | 610 | const canvasCtx = notNull(this.canvasCtx); 611 | canvasCtx.putImageData(imageData, 0, 0); 612 | }, 613 | 614 | init() { 615 | const canvasCtx = notNull(SimulatorUI.getCanvas().getContext('2d')); 616 | this.canvasCtx = canvasCtx; 617 | this.imageData = canvasCtx.createImageData(Display.SCREEN_WIDTH, Display.SCREEN_HEIGHT); 618 | }, 619 | }; 620 | 621 | // 4.INPUT 622 | 623 | /* 624 | We make mouse and keyboard input available to our simulated computer by setting 625 | certain locations in memory the current keyboard and mouse states before each 626 | CPU operation. 627 | 628 | Because the browser provides an event-based API for input, we need to listen for 629 | relevent keyboard and mouse events and keep track of their state and expose it 630 | to the simulated computer. 631 | */ 632 | const Input = { 633 | keysPressed: new Set(), 634 | mouseDown: false, 635 | mouseX: 0, 636 | mouseY: 0, 637 | 638 | init() { 639 | if (!document.body) throw new Error('DOM not ready'); 640 | 641 | document.body.onkeydown = (event) => { 642 | this.keysPressed.add(event.which); 643 | }; 644 | document.body.onkeyup = (event) => { 645 | this.keysPressed.delete(event.which); 646 | }; 647 | 648 | document.body.onmousedown = () => { 649 | this.mouseDown = true; 650 | }; 651 | document.body.onmouseup = () => { 652 | this.mouseDown = false; 653 | }; 654 | 655 | const screenPageY = SimulatorUI.getCanvas().getBoundingClientRect().top + window.scrollY; 656 | const screenPageX = SimulatorUI.getCanvas().getBoundingClientRect().left + window.scrollX; 657 | SimulatorUI.getCanvas().onmousemove = (event) => { 658 | this.mouseX = Math.floor((event.pageX - screenPageX) / Display.SCREEN_PIXEL_SCALE); 659 | this.mouseY = Math.floor((event.pageY - screenPageY) / Display.SCREEN_PIXEL_SCALE); 660 | }; 661 | }, 662 | 663 | updateInputs() { 664 | const mostRecentKeys = Array.from(this.keysPressed.values()).reverse(); 665 | 666 | Memory.ram[Memory.KEYCODE_0_ADDRESS] = mostRecentKeys[0] || 0; 667 | Memory.ram[Memory.KEYCODE_1_ADDRESS] = mostRecentKeys[1] || 0; 668 | Memory.ram[Memory.KEYCODE_2_ADDRESS] = mostRecentKeys[2] || 0; 669 | Memory.ram[Memory.MOUSE_BUTTON_ADDRESS] = this.mouseDown ? 1 : 0; 670 | Memory.ram[Memory.MOUSE_X_ADDRESS] = this.mouseX; 671 | Memory.ram[Memory.MOUSE_Y_ADDRESS] = this.mouseY; 672 | Memory.ram[Memory.MOUSE_PIXEL_ADDRESS] = Memory.VIDEO_MEMORY_START + (Math.floor(this.mouseY)) * Display.SCREEN_WIDTH + Math.floor(this.mouseX); 673 | Memory.ram[Memory.RANDOM_NUMBER_ADDRESS] = Math.floor(Math.random() * 255); 674 | Memory.ram[Memory.CURRENT_TIME_ADDRESS] = Date.now(); 675 | }, 676 | }; 677 | 678 | // 5.AUDIO 679 | 680 | const AudioContext = 681 | window.AudioContext || // Default 682 | window.webkitAudioContext; // Safari and old versions of Chrome 683 | 684 | const Audio = { 685 | WAVETYPES: { 686 | '0': 'square', 687 | '1': 'sawtooth', 688 | '2': 'triangle', 689 | '3': 'sine', 690 | }, 691 | 692 | MAX_GAIN: 0.15, 693 | 694 | audioCtx: new AudioContext(), 695 | 696 | audioChannels: [], 697 | 698 | addAudioChannel(wavetypeAddr, freqAddr, volAddr) { 699 | const oscillatorNode = this.audioCtx.createOscillator(); 700 | const gainNode = this.audioCtx.createGain(); 701 | oscillatorNode.connect(gainNode); 702 | gainNode.connect(this.audioCtx.destination); 703 | 704 | const state = { 705 | gain: 0, 706 | oscillatorType: 'square', 707 | frequency: 440, 708 | }; 709 | 710 | gainNode.gain.value = state.gain; 711 | oscillatorNode.type = state.oscillatorType; 712 | oscillatorNode.frequency.value = state.frequency; 713 | oscillatorNode.start(); 714 | 715 | return this.audioChannels.push({ 716 | state, 717 | wavetypeAddr, 718 | freqAddr, 719 | volAddr, 720 | gainNode, 721 | oscillatorNode, 722 | }); 723 | }, 724 | 725 | updateAudio() { 726 | this.audioChannels.forEach(channel => { 727 | const frequency = (Memory.ram[channel.freqAddr] || 0) / 1000; 728 | const gain = !CPU.running ? 0 : (Memory.ram[channel.volAddr] || 0) / 100 * this.MAX_GAIN; 729 | const oscillatorType = this.WAVETYPES[Memory.ram[channel.wavetypeAddr] || 0]; 730 | 731 | const {state} = channel; 732 | if (state.gain !== gain) { 733 | channel.gainNode.gain.setValueAtTime(gain, this.audioCtx.currentTime); 734 | state.gain = gain; 735 | } 736 | if (state.oscillatorType !== oscillatorType) { 737 | channel.oscillatorNode.type = oscillatorType; 738 | state.oscillatorType = oscillatorType; 739 | } 740 | if (state.frequency !== frequency) { 741 | channel.oscillatorNode.frequency.setValueAtTime(frequency, this.audioCtx.currentTime); 742 | state.frequency = frequency; 743 | } 744 | }); 745 | }, 746 | 747 | init() { 748 | this.addAudioChannel( 749 | Memory.AUDIO_CH1_WAVETYPE_ADDRESS, 750 | Memory.AUDIO_CH1_FREQUENCY_ADDRESS, 751 | Memory.AUDIO_CH1_VOLUME_ADDRESS 752 | ); 753 | this.addAudioChannel( 754 | Memory.AUDIO_CH2_WAVETYPE_ADDRESS, 755 | Memory.AUDIO_CH2_FREQUENCY_ADDRESS, 756 | Memory.AUDIO_CH2_VOLUME_ADDRESS 757 | ); 758 | this.addAudioChannel( 759 | Memory.AUDIO_CH3_WAVETYPE_ADDRESS, 760 | Memory.AUDIO_CH3_FREQUENCY_ADDRESS, 761 | Memory.AUDIO_CH3_VOLUME_ADDRESS 762 | ); 763 | }, 764 | }; 765 | 766 | // 6.ASSEMBLER 767 | 768 | /* 769 | We use a simple text-based language to input our program. This is our 'assembly 770 | language'. We need to convert it into a form which is made up of only numerical 771 | values so we can load it into our computer's Memory. This is a two step process: 772 | 773 | 1. parse program text into an array of objects representing our instructions and 774 | their operands. 775 | 2. convert the objects into numeric values to be interpreted by the CPU. This is 776 | our 'machine code'. 777 | 778 | We parse the program text into tokens by splitting the text into lines, then 779 | splitting those lines into tokens (words), which gives us to an instruction name 780 | and operands for that instruction, from each line. 781 | */ 782 | 783 | const Assembler = { 784 | // we'll keep a map of instructions which take a label as an operand so we 785 | // know when to substitute an operand for the corresponding label address 786 | instructionsLabelOperands: new Map(), 787 | 788 | initInstructionsLabelOperands() { 789 | Object.keys(CPU.instructions).forEach(name => { 790 | const labelOperandIndex = CPU.instructions[name].operands.findIndex(operand => 791 | operand[1] === 'label' 792 | ); 793 | if (labelOperandIndex > -1) { 794 | this.instructionsLabelOperands.set(name, labelOperandIndex); 795 | } 796 | }); 797 | }, 798 | 799 | // we break our program code into lines, then break those lines into 'tokens', 800 | // and then 'parse' that line of tokens into an instruction plus its operands 801 | parseProgramText(programText) { 802 | const programInstructions = []; 803 | const lines = programText.split('\n'); 804 | let line, i; 805 | try { 806 | for (i = 0; i < lines.length; i++) { 807 | line = lines[i]; 808 | const instruction = {name: '', operands: []}; 809 | let tokens = line.replace(/;.*$/, '') // strip comments 810 | .split(' '); 811 | for (let token of tokens) { 812 | // skip empty tokens 813 | if (token == null || token == "") { 814 | continue; 815 | } 816 | // first token 817 | if (!instruction.name) { 818 | // special case for labels 819 | if (token.endsWith(':')) { 820 | instruction.name = 'label'; 821 | instruction.operands.push(token.slice(0, token.length - 1)); 822 | break; 823 | } 824 | 825 | instruction.name = token; // instruction name token 826 | } else { 827 | // handle text operands 828 | if ( 829 | ( 830 | // define name 831 | instruction.name === 'define' && 832 | instruction.operands.length === 0 833 | ) || ( 834 | // label used as operand 835 | this.instructionsLabelOperands.get(instruction.name) === instruction.operands.length 836 | ) 837 | ) { 838 | instruction.operands.push(token); 839 | continue; 840 | } 841 | 842 | // try to parse number operands 843 | const number = parseInt(token, 10); 844 | if (Number.isNaN(number)) { 845 | instruction.operands.push(token); 846 | } else { 847 | instruction.operands.push(number); 848 | } 849 | } 850 | } 851 | 852 | // validate number of operands given 853 | if ( 854 | instruction.name && 855 | instruction.name !== 'label' && 856 | instruction.name !== 'data' && 857 | instruction.name !== 'define' 858 | ) { 859 | const expectedOperands = CPU.instructions[instruction.name].operands; 860 | if (instruction.operands.length !== expectedOperands.length) { 861 | const error = new Error( 862 | `Wrong number of operands for instruction ${instruction.name} 863 | got ${instruction.operands.length}, expected ${expectedOperands.length} 864 | at line ${i+1}: '${line}'` 865 | ); 866 | error.isException = true; 867 | throw error; 868 | } 869 | } 870 | 871 | // if instruction was found on this line, add it to the program 872 | if (instruction.name) { 873 | programInstructions.push(instruction); 874 | } 875 | } 876 | } catch (err) { 877 | if (err.isException) throw err; // validation error 878 | // otherwise it must be a parsing/syntax error 879 | throw new Error(`Syntax error on program line ${i+1}: '${line}'`); 880 | } 881 | programInstructions.push({name: 'halt', operands: []}); 882 | return programInstructions; 883 | }, 884 | 885 | /* 886 | Having parsed our program text into an array of objects containing instruction 887 | name and the operands to the instruction, we need to turn those objects into 888 | numeric values we can store in the computer's memory, and load them in there. 889 | */ 890 | assembleAndLoadProgram(programInstructions) { 891 | // 'label' is a special case – it's not really an instruction which the CPU 892 | // understands. Instead, it's a marker for the location of the next 893 | // instruction, which we can substitute for the actual location once we know 894 | // the memory locations in the assembled program which the labels refer to. 895 | const labelAddresses = {}; 896 | let labelAddress = Memory.PROGRAM_START; 897 | for (let instruction of programInstructions) { 898 | if (instruction.name === 'label') { 899 | const labelName = instruction.operands[0]; 900 | labelAddresses[labelName] = labelAddress; 901 | } else if (instruction.name === 'define') { 902 | continue; 903 | } else { 904 | // advance labelAddress by the length of the instruction and its operands 905 | labelAddress += 1 + instruction.operands.length; 906 | } 907 | } 908 | 909 | const defines = {}; 910 | 911 | // load instructions and operands into memory 912 | let loadingAddress = Memory.PROGRAM_START; 913 | for (let instruction of programInstructions) { 914 | if (instruction.name === 'label') { 915 | continue; 916 | } 917 | if (instruction.name === 'define') { 918 | defines[instruction.operands[0]] = instruction.operands[1]; 919 | continue; 920 | } 921 | 922 | if (instruction.name === 'data') { 923 | for (var i = 0; i < instruction.operands.length; i++) { 924 | Memory.ram[loadingAddress++] = instruction.operands[i]; 925 | } 926 | continue; 927 | } 928 | 929 | // for each instruction, we first write the relevant opcode to memory 930 | const opcode = CPU.instructionsToOpcodes.get(instruction.name); 931 | if (!opcode) { 932 | throw new Error(`No opcode found for instruction '${instruction.name}'`); 933 | } 934 | Memory.ram[loadingAddress++] = opcode; 935 | 936 | // then, we write the operands for instruction to memory 937 | const operands = instruction.operands.slice(0); 938 | 939 | // replace labels used as operands with actual memory address 940 | if (this.instructionsLabelOperands.has(instruction.name)) { 941 | const labelOperandIndex = this.instructionsLabelOperands.get(instruction.name); 942 | if (typeof labelOperandIndex !== 'number') throw new Error('expected number'); 943 | const labelName = instruction.operands[labelOperandIndex]; 944 | const labelAddress = labelAddresses[labelName]; 945 | if (!labelAddress) { 946 | throw new Error(`unknown label '${labelName}'`); 947 | } 948 | operands[labelOperandIndex] = labelAddress; 949 | } 950 | 951 | for (var i = 0; i < operands.length; i++) { 952 | let value = null; 953 | if (typeof operands[i] === 'string') { 954 | if (operands[i] in defines) { 955 | value = defines[operands[i]]; 956 | } else { 957 | throw new Error(`'${operands[i]}' not defined`); 958 | } 959 | } else { 960 | value = operands[i]; 961 | } 962 | 963 | Memory.ram[loadingAddress++] = value; 964 | } 965 | } 966 | }, 967 | 968 | init() { 969 | this.initInstructionsLabelOperands(); 970 | } 971 | }; 972 | 973 | // 7.SIMULATION CONTROL 974 | 975 | const Simulation = { 976 | CYCLES_PER_YIELD: 997, 977 | 978 | delayBetweenCycles: 0, 979 | 980 | loop() { 981 | if (Simulation.delayBetweenCycles === 0) { 982 | // running full speed, execute a bunch of instructions before yielding 983 | // to the JS event loop, to achieve decent 'real time' execution speed 984 | for (var i = 0; i < Simulation.CYCLES_PER_YIELD; i++) { 985 | if (!CPU.running) { 986 | Simulation.stop(); 987 | break; 988 | } 989 | CPU.step(); 990 | } 991 | } else { 992 | // run only one execution before yielding to the JS event loop so screen 993 | // and UI changes can be shown, and new mouse and keyboard input taken 994 | CPU.step(); 995 | SimulatorUI.updateUI(); 996 | } 997 | Simulation.updateOutputs(); 998 | if (CPU.running) { 999 | setTimeout(Simulation.loop, Simulation.delayBetweenCycles); 1000 | } 1001 | }, 1002 | 1003 | run() { 1004 | CPU.running = true; 1005 | SimulatorUI.updateUI(); 1006 | SimulatorUI.updateSpeedUI(); 1007 | this.loop(); 1008 | }, 1009 | 1010 | stop() { 1011 | CPU.running = false; 1012 | SimulatorUI.updateUI(); 1013 | SimulatorUI.updateSpeedUI(); 1014 | }, 1015 | 1016 | updateOutputs() { 1017 | Display.drawScreen(); 1018 | Audio.updateAudio(); 1019 | }, 1020 | 1021 | loadProgramAndReset() { 1022 | /* 1023 | In a real computer, memory addresses which have never had any value set are 1024 | considered 'uninitialized', and might contain any garbage value, but to keep 1025 | our simulation simple we're going to initialize every location with the value 1026 | 0. However, just like in a real computer, in our simulation it is possible 1027 | for us to mistakenly read from the wrong place in memory if we have a bug in 1028 | our simulated program where we get the memory address wrong. 1029 | */ 1030 | for (var i = 0; i < Memory.TOTAL_MEMORY_SIZE; i++) { 1031 | Memory.ram[i] = 0; 1032 | } 1033 | 1034 | const programText = SimulatorUI.getProgramText(); 1035 | try { 1036 | Assembler.assembleAndLoadProgram(Assembler.parseProgramText(programText)); 1037 | } catch (err) { 1038 | alert(err.message); 1039 | console.error(err); 1040 | } 1041 | SimulatorUI.setLoadedProgramText(programText); 1042 | 1043 | CPU.reset(); 1044 | this.updateOutputs(); 1045 | SimulatorUI.updateProgramMemoryView(); 1046 | SimulatorUI.updateUI(); 1047 | SimulatorUI.updateSpeedUI(); 1048 | }, 1049 | 1050 | stepOnce() { 1051 | CPU.running = true; 1052 | CPU.step(); 1053 | CPU.running = false; 1054 | this.updateOutputs(); 1055 | SimulatorUI.updateUI(); 1056 | }, 1057 | 1058 | runStop() { 1059 | if (CPU.running) { 1060 | this.stop(); 1061 | } else { 1062 | this.run(); 1063 | } 1064 | }, 1065 | } 1066 | 1067 | // 8.BUILT-IN PROGRAMS 1068 | 1069 | const PROGRAMS = { 1070 | 'Add': 1071 | ` 1072 | define a 0 1073 | define b 1 1074 | define result 2 1075 | 1076 | copy_to_from_constant a 4 1077 | copy_to_from_constant b 4 1078 | add a b result 1079 | ; look at memory location 2, you should now see '8' 1080 | `, 1081 | 1082 | 'RandomPixels': 1083 | ` 1084 | define videoStartAddr 2100 1085 | define videoEndAddr 3000 1086 | define randomNumberAddr 2050 1087 | define numColors 16 1088 | 1089 | FillScreen: 1090 | define fillScreenPtr 0 ; address at which store address of current screen pixel in loop 1091 | copy_to_from_constant fillScreenPtr videoStartAddr ; initialize to point to first pixel 1092 | jump_to FillScreenLoop 1093 | 1094 | FillScreenLoop: 1095 | define tempAddr 1 ; address to use for temporary storage 1096 | 1097 | ; modulo random value by number of colors in palette to get a random color... 1098 | modulo_constant randomNumberAddr numColors tempAddr 1099 | 1100 | ; ...and write it to current screen pixel, eg. the address pointed to by fillScreenPtr 1101 | copy_into_ptr_from fillScreenPtr tempAddr 1102 | 1103 | ; increment pointer to point to next screen pixel address 1104 | add_constant fillScreenPtr 1 fillScreenPtr 1105 | 1106 | branch_if_not_equal_constant fillScreenPtr videoEndAddr FillScreenLoop ; if not finished, repeat 1107 | jump_to FillScreen ; filled screen, now start again from the top 1108 | `, 1109 | 1110 | 'Paint': 1111 | `Init: 1112 | 1113 | define colorPickerStartAddr 2100 1114 | define colorPickerEndAddr 2116 1115 | define mousePixelAddr 2012 1116 | define mouseButtonAddr 2013 1117 | define currentColorAddr 0 1118 | define loopCounterAddr 1 1119 | define numColors 16 1120 | define comparisonResultAddr 4 1121 | define lastClickedAddr 2 1122 | define lessThanResult -1 1123 | 1124 | copy_to_from_constant loopCounterAddr colorPickerStartAddr ; init loop counter to start of video memory 1125 | copy_to_from_constant currentColorAddr 0 ; we'll use this while drawing color picker 1126 | DrawColorPickerLoop: 1127 | copy_into_ptr_from loopCounterAddr currentColorAddr 1128 | add_constant loopCounterAddr 1 loopCounterAddr 1129 | add_constant currentColorAddr 1 currentColorAddr 1130 | branch_if_not_equal_constant loopCounterAddr colorPickerEndAddr DrawColorPickerLoop 1131 | copy_to_from_constant currentColorAddr 3; initial color (green) 1132 | 1133 | MainLoop: 1134 | branch_if_equal_constant mouseButtonAddr loopCounterAddr HandleClick 1135 | jump_to MainLoop 1136 | 1137 | HandleClick: 1138 | copy_to_from lastClickedAddr mousePixelAddr ; store mouse location in case it changes 1139 | compare_constant lastClickedAddr colorPickerEndAddr comparisonResultAddr 1140 | branch_if_equal_constant comparisonResultAddr lessThanResult SelectColor 1141 | jump_to PaintAtCursor 1142 | 1143 | SelectColor: 1144 | subtract_constant lastClickedAddr colorPickerStartAddr currentColorAddr 1145 | jump_to MainLoop 1146 | 1147 | PaintAtCursor: 1148 | copy_into_ptr_from lastClickedAddr currentColorAddr ; set pixel at mouse cursor to color at currentColorAddr 1149 | jump_to MainLoop 1150 | `, 1151 | 1152 | 'ChocolateRain': ` 1153 | define accumulatorAddr 0 1154 | define dataTempAddr 1 1155 | define musicPlayheadPtr 2 1156 | define startTimeAddr 3 1157 | define channelDestinationPtr 4 1158 | define currentTimeAddr 2051 1159 | define beatLengthInMS 200 1160 | define ch1WaveTypeAddr 3000 1161 | define ch1FreqAddr 3001 1162 | define ch2WaveTypeAddr 3003 1163 | 1164 | copy_to_from_constant ch1WaveTypeAddr 3 ; sine 1165 | copy_to_from_constant ch2WaveTypeAddr 0 ; sawtooth 1166 | 1167 | Reset: 1168 | copy_to_from startTimeAddr currentTimeAddr ; keep time started to calculate time elapsed 1169 | 1170 | copy_address_of_label musicPlayheadPtr MusicData 1171 | 1172 | WaitForEvent: 1173 | ; calculate current beat from time 1174 | subtract currentTimeAddr startTimeAddr accumulatorAddr 1175 | divide_constant accumulatorAddr beatLengthInMS accumulatorAddr 1176 | copy_to_from_ptr dataTempAddr musicPlayheadPtr 1177 | 1178 | branch_if_equal_constant dataTempAddr -1 Reset 1179 | compare accumulatorAddr dataTempAddr dataTempAddr 1180 | branch_if_not_equal_constant dataTempAddr -1 PlayNote 1181 | jump_to WaitForEvent 1182 | 1183 | PlayNote: 1184 | ; advance source pointer to channel data 1185 | add_constant musicPlayheadPtr 1 musicPlayheadPtr 1186 | 1187 | ; move dest pointer to frequency address for channel 1188 | copy_to_from_constant channelDestinationPtr ch1FreqAddr ; move to ch1FreqAddr 1189 | ; in dataTempAddr, calculate relative offset of channel's frequency address from ch1FreqAddr 1190 | copy_to_from_ptr dataTempAddr musicPlayheadPtr 1191 | multiply_constant dataTempAddr 3 dataTempAddr 1192 | ; increment pointer by channel offset to point to correct channel's frequency address 1193 | add channelDestinationPtr dataTempAddr channelDestinationPtr 1194 | 1195 | add_constant musicPlayheadPtr 1 musicPlayheadPtr ; advance source pointer to frequency data 1196 | 1197 | ; copy frequency 1198 | copy_to_from_ptr dataTempAddr musicPlayheadPtr 1199 | copy_into_ptr_from channelDestinationPtr dataTempAddr 1200 | 1201 | ; move destination pointer to volume address for channel 1202 | add_constant channelDestinationPtr 1 channelDestinationPtr 1203 | ; advance source pointer to volume dataTempAddr 1204 | add_constant musicPlayheadPtr 1 musicPlayheadPtr 1205 | 1206 | ; copy volume 1207 | copy_to_from_ptr dataTempAddr musicPlayheadPtr 1208 | copy_into_ptr_from channelDestinationPtr dataTempAddr 1209 | 1210 | add_constant musicPlayheadPtr 1 musicPlayheadPtr ; advance to next music event 1211 | jump_to WaitForEvent 1212 | 1213 | 1214 | MusicData: 1215 | data 0 1 195997 53 1216 | data 0 0 622253 53 1217 | data 0 0 130812 53 1218 | data 1 0 622253 0 1219 | data 1 0 622253 58 1220 | data 2 1 195997 0 1221 | data 2 1 155563 56 1222 | data 2 0 130812 0 1223 | data 2 0 622253 0 1224 | data 2 0 783990 68 1225 | data 3 0 783990 0 1226 | data 3 0 523251 49 1227 | data 4 1 155563 0 1228 | data 4 1 195997 64 1229 | data 4 0 523251 0 1230 | data 4 0 698456 64 1231 | data 4 0 233081 52 1232 | data 5 0 698456 0 1233 | data 5 0 466163 50 1234 | data 6 1 195997 0 1235 | data 6 0 233081 0 1236 | data 6 0 466163 0 1237 | data 6 0 587329 64 1238 | data 7 0 587329 0 1239 | data 7 0 622253 60 1240 | data 7 0 195997 51 1241 | data 8 0 622253 0 1242 | data 8 0 311126 43 1243 | data 9 0 195997 0 1244 | data 9 0 311126 0 1245 | data 9 0 523251 69 1246 | data 10 0 523251 0 1247 | data 10 0 391995 50 1248 | data 11 0 391995 0 1249 | data 11 0 587329 71 1250 | data 11 0 146832 50 1251 | data 12 0 146832 0 1252 | data 12 0 587329 0 1253 | data 12 0 391995 50 1254 | data 13 0 391995 0 1255 | data 13 0 466163 61 1256 | data 14 0 466163 0 1257 | data 14 0 523251 63 1258 | data 14 0 155563 50 1259 | data 15 1 146832 50 1260 | data 15 0 523251 0 1261 | data 15 0 391995 51 1262 | data 16 1 146832 0 1263 | data 16 1 155563 57 1264 | data 16 0 155563 0 1265 | data 16 0 391995 0 1266 | data 16 0 622253 68 1267 | data 16 0 207652 60 1268 | data 17 0 622253 0 1269 | data 17 0 622253 60 1270 | data 18 1 155563 0 1271 | data 18 1 195997 62 1272 | data 18 0 207652 0 1273 | data 18 0 622253 0 1274 | data 18 0 783990 63 1275 | data 18 0 311126 70 1276 | data 19 1 195997 0 1277 | data 19 1 174614 54 1278 | data 19 0 783990 0 1279 | data 19 0 523251 46 1280 | data 20 1 174614 0 1281 | data 20 0 311126 0 1282 | data 20 0 523251 0 1283 | data 20 0 698456 66 1284 | data 20 0 293664 57 1285 | data 21 1 146832 55 1286 | data 21 0 698456 0 1287 | data 21 0 466163 51 1288 | data 22 0 293664 0 1289 | data 22 0 466163 0 1290 | data 22 0 587329 65 1291 | data 22 0 233081 52 1292 | data 23 1 146832 0 1293 | data 23 1 155563 59 1294 | data 23 0 233081 0 1295 | data 23 0 587329 0 1296 | data 23 0 622253 63 1297 | data 23 0 261625 65 1298 | data 24 0 622253 0 1299 | data 24 0 311126 41 1300 | data 25 1 155563 0 1301 | data 25 1 130812 57 1302 | data 25 0 261625 0 1303 | data 25 0 311126 0 1304 | data 25 0 523251 66 1305 | data 25 0 195997 58 1306 | data 26 0 523251 0 1307 | data 26 0 391995 53 1308 | data 27 1 130812 0 1309 | data 27 1 146832 60 1310 | data 27 0 195997 0 1311 | data 27 0 391995 0 1312 | data 27 0 587329 69 1313 | data 27 0 233081 63 1314 | data 28 0 587329 0 1315 | data 28 0 391995 52 1316 | data 29 1 146832 0 1317 | data 29 1 116540 56 1318 | data 29 0 233081 0 1319 | data 29 0 391995 0 1320 | data 29 0 466163 59 1321 | data 30 1 116540 0 1322 | data 30 1 130812 63 1323 | data 30 0 466163 0 1324 | data 30 0 523251 61 1325 | data 30 0 261625 56 1326 | data 31 0 523251 0 1327 | data 31 0 391995 50 1328 | data 32 1 130812 0 1329 | data 32 1 233081 65 1330 | data 32 0 261625 0 1331 | data 32 0 391995 0 1332 | data 32 0 622253 73 1333 | data 32 0 207652 53 1334 | data 33 0 622253 0 1335 | data 33 0 622253 60 1336 | data 34 1 233081 0 1337 | data 34 1 155563 52 1338 | data 34 0 207652 0 1339 | data 34 0 622253 0 1340 | data 34 0 783990 64 1341 | data 35 0 783990 0 1342 | data 35 0 523251 50 1343 | data 36 1 155563 0 1344 | data 36 1 195997 62 1345 | data 36 0 523251 0 1346 | data 36 0 932327 71 1347 | data 36 0 174614 53 1348 | data 37 0 932327 0 1349 | data 37 0 466163 43 1350 | data 38 1 195997 0 1351 | data 38 0 174614 0 1352 | data 38 0 466163 0 1353 | data 38 0 587329 62 1354 | data 39 0 587329 0 1355 | data 39 0 622253 60 1356 | data 39 0 261625 50 1357 | data 40 0 622253 0 1358 | data 40 0 311126 43 1359 | data 41 0 261625 0 1360 | data 41 0 311126 0 1361 | data 41 0 523251 66 1362 | data 42 0 523251 0 1363 | data 42 0 391995 53 1364 | data 43 0 391995 0 1365 | data 43 0 587329 68 1366 | data 43 0 293664 55 1367 | data 44 0 293664 0 1368 | data 44 0 587329 0 1369 | data 44 0 391995 49 1370 | data 45 0 391995 0 1371 | data 45 0 466163 67 1372 | data 46 0 466163 0 1373 | data 46 0 523251 67 1374 | data 46 0 311126 50 1375 | data 47 1 146832 54 1376 | data 47 0 523251 0 1377 | data 47 0 523251 61 1378 | data 48 1 146832 0 1379 | data 48 1 155563 60 1380 | data 48 0 311126 0 1381 | data 48 0 523251 0 1382 | data 48 0 1046502 71 1383 | data 48 0 207652 53 1384 | data 49 0 1046502 0 1385 | data 49 0 523251 45 1386 | data 50 1 155563 0 1387 | data 50 1 195997 64 1388 | data 50 0 207652 0 1389 | data 50 0 523251 0 1390 | data 50 0 783990 68 1391 | data 51 1 195997 0 1392 | data 51 1 174614 60 1393 | data 51 0 783990 0 1394 | data 51 0 783990 58 1395 | data 52 1 174614 0 1396 | data 52 0 783990 0 1397 | data 52 0 932327 64 1398 | data 52 0 195997 53 1399 | data 53 1 146832 50 1400 | data 53 0 932327 0 1401 | data 53 0 466163 43 1402 | data 54 0 195997 0 1403 | data 54 0 466163 0 1404 | data 54 0 698456 64 1405 | data 55 1 146832 0 1406 | data 55 1 155563 58 1407 | data 55 0 698456 0 1408 | data 55 0 783990 64 1409 | data 55 0 207652 51 1410 | data 56 0 783990 0 1411 | data 56 0 415304 43 1412 | data 57 1 155563 0 1413 | data 57 1 130812 56 1414 | data 57 0 207652 0 1415 | data 57 0 415304 0 1416 | data 57 0 622253 67 1417 | data 58 0 622253 0 1418 | data 58 0 523251 56 1419 | data 59 1 130812 0 1420 | data 59 1 146832 57 1421 | data 59 0 523251 0 1422 | data 59 0 698456 71 1423 | data 59 0 233081 57 1424 | data 60 0 233081 0 1425 | data 60 0 698456 0 1426 | data 60 0 466163 49 1427 | data 61 1 146832 0 1428 | data 61 1 116540 52 1429 | data 61 0 466163 0 1430 | data 61 0 587329 64 1431 | data 62 1 116540 0 1432 | data 62 1 130812 57 1433 | data 62 0 587329 0 1434 | data 62 0 622253 62 1435 | data 62 0 261625 56 1436 | data 64 1 130812 0 1437 | data 64 1 195997 64 1438 | data 64 0 261625 0 1439 | data 64 0 622253 0 1440 | data 64 0 622253 61 1441 | data 64 0 130812 52 1442 | data -1 1443 | `, 1444 | 1445 | 'Custom 1': '', 1446 | 'Custom 2': '', 1447 | 'Custom 3': '', 1448 | }; 1449 | 1450 | // boring code for rendering user interface of the simulator 1451 | // not really important for understanding how computers work 1452 | const UI = { 1453 | $(selector) { 1454 | const el = document.querySelector(selector); 1455 | if (el == null) throw new Error(`couldn't find selector '${selector}'`); 1456 | return el; 1457 | }, 1458 | 1459 | $Input(selector) { 1460 | const el = UI.$(selector); 1461 | if (el instanceof HTMLInputElement) return el; 1462 | throw new Error('expected HTMLInputElement'); 1463 | }, 1464 | $TextArea(selector) { 1465 | const el = UI.$(selector); 1466 | if (el instanceof HTMLTextAreaElement) return el; 1467 | throw new Error('expected HTMLTextAreaElement'); 1468 | }, 1469 | $Button(selector) { 1470 | const el = UI.$(selector); 1471 | if (el instanceof HTMLButtonElement) return el; 1472 | throw new Error('expected HTMLButtonElement'); 1473 | }, 1474 | $Canvas(selector) { 1475 | const el = UI.$(selector); 1476 | if (el instanceof HTMLCanvasElement) return el; 1477 | throw new Error('expected HTMLCanvasElement'); 1478 | }, 1479 | $Select(selector) { 1480 | const el = UI.$(selector); 1481 | if (el instanceof HTMLSelectElement) return el; 1482 | throw new Error('expected HTMLSelectElement'); 1483 | }, 1484 | 1485 | virtualizedScrollView(container, containerHeight, itemHeight, numItems, renderItems) { 1486 | Object.assign(container.style, { 1487 | height: `${containerHeight}px`, 1488 | overflow: 'auto', 1489 | }); 1490 | const content = document.createElement('div'); 1491 | Object.assign(content.style, { 1492 | height: `${itemHeight * numItems}px`, 1493 | overflow: 'hidden', 1494 | }); 1495 | container.appendChild(content); 1496 | 1497 | const rows = document.createElement('div'); 1498 | content.appendChild(rows); 1499 | 1500 | const overscan = 10; // how many rows above/below viewport to render 1501 | 1502 | const renderRowsInView = () => requestAnimationFrame(() => { 1503 | const start = Math.max(0, Math.floor(container.scrollTop / itemHeight) - overscan); 1504 | const end = Math.min(numItems, Math.ceil((container.scrollTop + containerHeight) / itemHeight) + overscan); 1505 | const offsetTop = start * itemHeight; 1506 | 1507 | rows.style.transform = `translateY(${offsetTop}px)`; 1508 | rows.innerHTML = renderItems(start, end); 1509 | }); 1510 | 1511 | container.onscroll = renderRowsInView; 1512 | 1513 | return renderRowsInView; 1514 | } 1515 | }; 1516 | 1517 | const SimulatorUI = { 1518 | selectedProgram: localStorage.getItem('selectedProgram') || 'RandomPixels', 1519 | 1520 | initUI() { 1521 | const programSelectorEl = UI.$Select('#programSelector'); 1522 | // init program selector 1523 | Object.keys(PROGRAMS).forEach(programName => { 1524 | const option = document.createElement('option'); 1525 | option.value = programName; 1526 | option.textContent = programName; 1527 | programSelectorEl.append(option); 1528 | }); 1529 | programSelectorEl.value = this.selectedProgram; 1530 | this.selectProgram(); 1531 | }, 1532 | 1533 | getProgramText() { 1534 | return UI.$TextArea('#program').value; 1535 | }, 1536 | 1537 | getCanvas() { 1538 | return UI.$Canvas('#canvas'); 1539 | }, 1540 | 1541 | initScreen(width, height, pixelScale) { 1542 | let imageRendering = 'pixelated'; 1543 | if (/firefox/i.test(navigator.userAgent)) { 1544 | imageRendering = '-moz-crisp-edges'; 1545 | } 1546 | Object.assign(SimulatorUI.getCanvas(), {width, height}); 1547 | // scale our (very low resolution) canvas up to a more viewable size using CSS transforms 1548 | // $FlowFixMe: ignore unknown property '-ms-interpolation-mode' 1549 | Object.assign(SimulatorUI.getCanvas().style, { 1550 | transformOrigin: 'top left', 1551 | transform: `scale(${pixelScale})`, 1552 | '-ms-interpolation-mode': 'nearest-neighbor', 1553 | imageRendering, 1554 | }); 1555 | }, 1556 | 1557 | loadedProgramText: '', 1558 | setLoadedProgramText(programText) { 1559 | this.loadedProgramText = programText; 1560 | UI.$Button('#loadProgramButton').disabled = true; 1561 | }, 1562 | 1563 | updateLoadProgramButton() { 1564 | UI.$Button('#loadProgramButton').disabled = this.loadedProgramText === this.getProgramText(); 1565 | }, 1566 | 1567 | selectProgram() { 1568 | this.selectedProgram = UI.$Select('#programSelector').value; 1569 | localStorage.setItem('selectedProgram', this.selectedProgram); 1570 | UI.$TextArea('#program').value = 1571 | localStorage.getItem(this.selectedProgram) || PROGRAMS[this.selectedProgram] || ''; 1572 | this.updateLoadProgramButton(); 1573 | }, 1574 | 1575 | editProgramText() { 1576 | if (this.selectedProgram.startsWith('Custom')) { 1577 | localStorage.setItem(this.selectedProgram, UI.$TextArea('#program').value); 1578 | } 1579 | this.updateLoadProgramButton(); 1580 | }, 1581 | 1582 | setSpeed() { 1583 | Simulation.delayBetweenCycles = -parseInt(UI.$Input('#speed').value, 10); 1584 | this.updateSpeedUI(); 1585 | }, 1586 | 1587 | setFullspeed() { 1588 | const fullspeedEl = UI.$Input('#fullspeed'); 1589 | if (fullspeedEl && fullspeedEl.checked) { 1590 | Simulation.delayBetweenCycles = 0; 1591 | } else { 1592 | Simulation.delayBetweenCycles = 1; 1593 | } 1594 | this.updateSpeedUI(); 1595 | }, 1596 | 1597 | updateSpeedUI() { 1598 | const fullspeed = Simulation.delayBetweenCycles === 0; 1599 | const runningAtFullspeed = CPU.running && fullspeed; 1600 | UI.$Input('#fullspeed').checked = fullspeed; 1601 | UI.$Input('#speed').value = String(-Simulation.delayBetweenCycles); 1602 | UI.$('#debugger').classList.toggle('fullspeed', runningAtFullspeed); 1603 | UI.$('#debuggerMessageArea').textContent = runningAtFullspeed ? 1604 | 'debug UI disabled when CPU.running at full speed' : ''; 1605 | }, 1606 | 1607 | updateUI() { 1608 | UI.$Input('#programCounter').value = String(CPU.programCounter); 1609 | if (CPU.halted) { 1610 | UI.$('#running').textContent = 'halted'; 1611 | UI.$Button('#stepButton').disabled = true; 1612 | UI.$Button('#runButton').disabled = true; 1613 | } else { 1614 | UI.$('#running').textContent = CPU.running ? 'running' : 'paused'; 1615 | UI.$Button('#stepButton').disabled = false; 1616 | UI.$Button('#runButton').disabled = false; 1617 | } 1618 | this.updateWorkingMemoryView(); 1619 | this.updateInputMemoryView(); 1620 | this.updateVideoMemoryView(); 1621 | this.updateAudioMemoryView(); 1622 | if (Simulation.delayBetweenCycles > 300 || !CPU.running) { 1623 | if (typeof this.scrollToProgramLine == 'function') { 1624 | this.scrollToProgramLine(Math.max(0, CPU.programCounter - Memory.PROGRAM_MEMORY_START - 3)); 1625 | } 1626 | } 1627 | }, 1628 | 1629 | updateWorkingMemoryView() { 1630 | const lines = []; 1631 | for (var i = Memory.WORKING_MEMORY_START; i < Memory.WORKING_MEMORY_END; i++) { 1632 | lines.push(`${i}: ${Memory.ram[i]}`); 1633 | } 1634 | UI.$TextArea('#workingMemoryView').textContent = lines.join('\n'); 1635 | }, 1636 | 1637 | scrollToProgramLine: (item) => {}, 1638 | updateProgramMemoryView() { 1639 | const lines = []; 1640 | for (var i = Memory.PROGRAM_MEMORY_START; i < Memory.PROGRAM_MEMORY_END; i++) { 1641 | const instruction = CPU.opcodesToInstructions.get(Memory.ram[i]); 1642 | lines.push(`${padRight(i, 4)}: ${padRight(Memory.ram[i], 8)} ${instruction || ''}`); 1643 | if (instruction) { 1644 | const operands = CPU.instructions[instruction].operands; 1645 | for (var j = 0; j < operands.length; j++) { 1646 | lines.push(`${padRight(i + 1 + j, 4)}: ${padRight(Memory.ram[i + 1 + j], 8)} ${operands[j][0]} (${operands[j][1]})`); 1647 | } 1648 | i += operands.length; 1649 | } 1650 | } 1651 | 1652 | const itemHeight = 14; 1653 | const renderProgramMemoryView = UI.virtualizedScrollView( 1654 | UI.$('#programMemoryView'), 1655 | 136, 1656 | itemHeight, 1657 | lines.length, 1658 | (start, end) => ( 1659 | lines.slice(start, end) 1660 | .map((l, i) => { 1661 | const current = Memory.PROGRAM_MEMORY_START + start + i === CPU.programCounter; 1662 | return ` 1663 |
${l}
1667 | `; 1668 | }) 1669 | .join('') 1670 | ) 1671 | ); 1672 | 1673 | this.scrollToProgramLine = (item) => { 1674 | UI.$('#programMemoryView').scrollTop = item * itemHeight; 1675 | renderProgramMemoryView(); 1676 | }; 1677 | 1678 | renderProgramMemoryView(); 1679 | }, 1680 | 1681 | updateInputMemoryView() { 1682 | UI.$TextArea('#inputMemoryView').textContent = 1683 | `${Memory.KEYCODE_0_ADDRESS}: ${padRight(Memory.ram[Memory.KEYCODE_0_ADDRESS], 8)} keycode 0 1684 | ${Memory.KEYCODE_1_ADDRESS}: ${padRight(Memory.ram[Memory.KEYCODE_1_ADDRESS], 8)} keycode 1 1685 | ${Memory.KEYCODE_2_ADDRESS}: ${padRight(Memory.ram[Memory.KEYCODE_2_ADDRESS], 8)} keycode 2 1686 | ${Memory.MOUSE_X_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_X_ADDRESS], 8)} mouse x 1687 | ${Memory.MOUSE_Y_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_Y_ADDRESS], 8)} mouse y 1688 | ${Memory.MOUSE_PIXEL_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_PIXEL_ADDRESS], 8)} mouse pixel 1689 | ${Memory.MOUSE_BUTTON_ADDRESS}: ${padRight(Memory.ram[Memory.MOUSE_BUTTON_ADDRESS], 8)} mouse button 1690 | ${Memory.RANDOM_NUMBER_ADDRESS}: ${padRight(Memory.ram[Memory.RANDOM_NUMBER_ADDRESS], 8)} random number 1691 | ${Memory.CURRENT_TIME_ADDRESS}: ${padRight(Memory.ram[Memory.CURRENT_TIME_ADDRESS], 8)} current time`; 1692 | }, 1693 | 1694 | updateVideoMemoryView() { 1695 | const lines = []; 1696 | for (var i = Memory.VIDEO_MEMORY_START; i < Memory.VIDEO_MEMORY_END; i++) { 1697 | lines.push(`${i}: ${Memory.ram[i]}`); 1698 | } 1699 | UI.$TextArea('#videoMemoryView').textContent = lines.join('\n'); 1700 | }, 1701 | 1702 | updateAudioMemoryView() { 1703 | UI.$TextArea('#audioMemoryView').textContent = 1704 | `${Memory.AUDIO_CH1_WAVETYPE_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH1_WAVETYPE_ADDRESS], 8)} audio ch1 wavetype 1705 | ${Memory.AUDIO_CH1_FREQUENCY_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH1_FREQUENCY_ADDRESS], 8)} audio ch1 frequency 1706 | ${Memory.AUDIO_CH1_VOLUME_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH1_VOLUME_ADDRESS], 8)} audio ch1 volume 1707 | ${Memory.AUDIO_CH2_WAVETYPE_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH2_WAVETYPE_ADDRESS], 8)} audio ch2 wavetype 1708 | ${Memory.AUDIO_CH2_FREQUENCY_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH2_FREQUENCY_ADDRESS], 8)} audio ch2 frequency 1709 | ${Memory.AUDIO_CH2_VOLUME_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH2_VOLUME_ADDRESS], 8)} audio ch2 volume 1710 | ${Memory.AUDIO_CH3_WAVETYPE_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH3_WAVETYPE_ADDRESS], 8)} audio ch3 wavetype 1711 | ${Memory.AUDIO_CH3_FREQUENCY_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH3_FREQUENCY_ADDRESS], 8)} audio ch3 frequency 1712 | ${Memory.AUDIO_CH3_VOLUME_ADDRESS}: ${padRight(Memory.ram[Memory.AUDIO_CH3_VOLUME_ADDRESS], 8)} audio ch3 volume`; 1713 | }, 1714 | } 1715 | 1716 | function clamp(val, min, max) { 1717 | return Math.min(min, Math.max(max, val)); 1718 | } 1719 | 1720 | function padRight(input, length) { 1721 | const str = input + ''; 1722 | let padded = str; 1723 | for (var i = str.length; i < length; i++) { 1724 | padded += " "; 1725 | } 1726 | return padded; 1727 | } 1728 | 1729 | /*:: declare function notNull(val: ?T): T; */ 1730 | function notNull(val) { 1731 | if (val != null) return val; 1732 | throw new Error('unexpected null'); 1733 | } 1734 | 1735 | CPU.init(); 1736 | Display.init(); 1737 | Input.init(); 1738 | Audio.init(); 1739 | Assembler.init(); 1740 | SimulatorUI.initScreen(Display.SCREEN_WIDTH, Display.SCREEN_HEIGHT, Display.SCREEN_PIXEL_SCALE); 1741 | SimulatorUI.initUI(); 1742 | Simulation.loadProgramAndReset(); 1743 | 1744 | // enable audio to work with chrome autoplay policy :'( 1745 | if (!document.body) throw new Error('DOM not ready'); 1746 | function resumeAudio() { 1747 | if (!document.body) throw new Error('DOM not ready'); 1748 | document.body.removeEventListener('click', resumeAudio); 1749 | Audio.audioCtx.resume() 1750 | } 1751 | document.body.addEventListener('click', resumeAudio); 1752 | --------------------------------------------------------------------------------