diff options
author | Drew DeVault <sir@cmpwn.com> | 2014-11-30 13:36:26 -0700 |
---|---|---|
committer | Drew DeVault <sir@cmpwn.com> | 2014-11-30 13:36:26 -0700 |
commit | 3d7eeb1ecd287aee9179ea6b818f07fbe0b38780 (patch) | |
tree | fe498091ad1167b0b5e6b510b6bcce532b62e1b5 | |
parent | b1f903b7ffd59d143957848c8ad04e6478f8706d (diff) |
Add crazy cool blog post
-rw-r--r-- | .gitmodules | 3 | ||||
m--------- | OpenTI | 0 | ||||
-rw-r--r-- | _layouts/post_toolchain.html | 27 | ||||
-rw-r--r-- | _posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md | 326 | ||||
-rw-r--r-- | css/base.css | 8 | ||||
-rw-r--r-- | css/toolchain.scss | 40 | ||||
-rw-r--r-- | js/ide_emu.js | 150 | ||||
-rw-r--r-- | js/toolchain.coffee | 244 | ||||
-rw-r--r-- | scas.data | 533 | ||||
-rw-r--r-- | sources/corelib-hello.asm | 36 | ||||
-rw-r--r-- | sources/fileman.asm | 15 | ||||
-rw-r--r-- | sources/helloworld.asm | 27 | ||||
-rw-r--r-- | tools/genkfs.js | 22 | ||||
-rw-r--r-- | tools/genkfs.js.mem | bin | 0 -> 1088 bytes | |||
-rw-r--r-- | tools/kpack.js | 22 | ||||
-rw-r--r-- | tools/kpack.js.mem | bin | 0 -> 4528 bytes | |||
-rw-r--r-- | tools/scas.js | 23 | ||||
-rw-r--r-- | tools/scas.js.mem | bin | 0 -> 7608 bytes | |||
-rw-r--r-- | tools/z80e.js | 26 | ||||
-rw-r--r-- | tools/z80e.js.mem | bin | 0 -> 11632 bytes |
20 files changed, 1502 insertions, 0 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e0a0492 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "OpenTI"] + path = OpenTI + url = git://github.com/KnightOS/OpenTI.git diff --git a/OpenTI b/OpenTI new file mode 160000 +Subproject 81e1d4372f5ca66f1becb9bdc7f619048ae549e diff --git a/_layouts/post_toolchain.html b/_layouts/post_toolchain.html new file mode 100644 index 0000000..a084d8e --- /dev/null +++ b/_layouts/post_toolchain.html @@ -0,0 +1,27 @@ +--- +layout: base +showTitle: true +--- + +<link rel="stylesheet" type="text/css" href="/css/toolchain.css" /> +<script src="//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3/ace.js"></script> +<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script> +<hr /> +{{ content }} +<div class="calculator-wrapper"> + <p style="margin: 0; text-align: center; background: #fff"><a href="#" id="hide-toolchain">Hide toolchain output</a></p> + <textarea disabled class="form-control" rows=10 placeholder="(toolchain output)" id="tool-log" style="width: 100%;"></textarea> + <div class="calculator"> + <canvas width="385" height="256" id="screen"></canvas> + </div> +</div> +<div id="disqus_thread"></div> +<script type="text/javascript"> +var disqus_shortname = 'drewdevaultblog'; +(function() { + var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; + dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; + (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); +})(); +</script> +<script data-main="/js/toolchain.js" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.15/require.min.js"></script> diff --git a/_posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md b/_posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md new file mode 100644 index 0000000..39a5048 --- /dev/null +++ b/_posts/2014-11-30-Porting-an-entire-toolchain-to-the-browser-with-emscripten.md @@ -0,0 +1,326 @@ +--- +# vim: tw=80 +layout: post_toolchain +title: Porting an entire desktop toolchain to the browser with Emscripten +--- + +Emscripten is pretty damn cool! It lets you write portable C and cross-compile +it to JavaScript so it'll run in a web browser. As the maintainer of +[KnightOS](http://www.knightos.org), I looked to emscripten as a potential means +of reducing the cost of entry for new developers hoping to target the OS. + +Side note: apologies to those with narrow displays. + +## Rationale for Emscripten + +There are several pieces of software in the toolchain that are required to write +and test software for KnightOS: + +* [scas](https://github.com/KnightOS/scas) - a z80 assembler +* [genkfs](https://github.com/KnightOS/genkfs) - generates KFS filesystem images +* [kpack](https://github.com/KnightOS/kpack) - packaging tool, like makepkg on Arch Linux +* [z80e](https://github.com/KnightOS/z80e) - a z80 calculator emulator + +You also need a copy of the latest kernel and any of your dependencies from +[packages.knightos.org](https://packages.knightos.org). Getting all of this is +not straightforward. On Linux and Mac, there are no official packages for any of +these tools. On Windows, there are still no official packages, and you have to +use Cygwin on top of that. The first step to writing KnightOS programs is to +manually compile and install several tools, which is a lot to ask of someone who +just wants to experiment. + +All of the tools in our toolchain are written in C. We saw Emscripten as an +opportunity to reduce all of this effort into simply firing up your web browser. +It works, too! Here's what was involved. + +>**Note**: Click the screen on the emulator to the left to give it your +>keyboard. Click away to take it back. You can use your arrow keys, F1-F5, +>enter, and escape (as MODE). + +## The final product + +Let's start by showing you what we've accomplished. It's now possible for +curious developers to try out KnightOS programming in their web browser. Of +course, they still have to do it in assembly, but we're [working on +that](https://github.com/KnightOS/kcc) 😉. Here's a "hello world" you can run in +your web browser: + +<div class="editor" data-source="/sources/helloworld.asm" data-file="main.asm"></div> + +We can also install new dependencies on the fly and use them in our programs. +Here's another program that draws the "hello world" message in a window. You +should install `core/corelib` first: + +<input type="text" id="package-name" value="core/corelib" /> +<button id="install-package">Install</button> + +<div class="editor" data-source="/sources/corelib-hello.asm" data-file="main.asm"></div> + +You can find more packages to try out on +[packages.knightos.org](https://packages.knightos.org). Here's another example, +this one launches the file manager. You'll have to install a few packages for it +to work: + +Install: +<button class="install-package-button" data-package="extra/fileman">extra/fileman</button> +<button class="install-package-button" data-package="core/configlib">core/configlib</button> +<button class="install-package-button" data-package="core/corelib">core/corelib</button> + +<div class="editor" data-source="/sources/fileman.asm" data-file="main.asm"></div> + +Feel free to edit any of these examples! You can run them again with the Run +button, of course. These resources might be useful if you want to play with this +some more: + +[z80 instruction set](http://www.z80.info/z80-op.txt) - [z80 assembly tutorial](http://tutorials.eeems.ca/ASMin28Days/lesson/toc.html) - [KnightOS reference documentation](http://www.knightos.org/documentation/reference) + +Note: our toolchain has some memory leaks, so eventually emscripten is going to +run out of memory and then you'll have to refresh. Sorry! + +## How all of the pieces fit together + +When you +loaded this page, a bunch of things happened. First, the [latest +release](https://github.com/KnightOS/kernel/releases) of the [KnightOS +kernel](https://github.com/KnightOS/kernel) was downloaded. Then all of the +emscripten ports of the toolchain were downloaded and loaded. The various +virtual filesystems were set up, and two packages were downloaded and installed: +`core/init`, and `core/kernel-headers`. Extracting those packages involves +copying them into kpack's virtual filesystem and running `kpack -e +path/to/package root/`. + +When you click "Run" on one of these text boxes, the contents of the text box is +written to `/main.asm` in the assembler's virtual filesystem. The package +installation process extracts headers to `/include/`, and scas itself is run +with `/main.asm -I/include -o /executable`, which assembles the program and +writes the output to `/executable`. + +Then we copy the executable into the genkfs filesystem (this is the tool that +generates filesystem images). We also copy the empty kernel into this +filesystem, as well as any of the packages we've installed. We then run `genkfs +/kernel.rom /root`, which creates a filesystem image from `/root` and bakes it +into `kernel.rom`. This produces a ready-to-emulate ROM image that we can load +into the z80e emulator on the left. + +## The emscripten details + +Porting all this stuff to emscripten wasn't straightforward. The easiest part +was cross-compiling all of them to JavaScript: + + cd build + emconfigure cmake .. + emmake make + +The process was basically that simple for each piece of software. There were +[a](https://github.com/KnightOS/genkfs/commit/c4eefa87a3b5bdbafcc6d971654608c594f779a1) +[few](https://github.com/KnightOS/scas/commit/d2044e7d7586a946422ce6493cc6dff01127d1c2) +[changes](https://github.com/KnightOS/scas/commit/8bc31af28e8419a9fa6c421147ea522935bd0df4) +made to some of the tools to fix a few problems. The hard part +came when I wanted to run all of them on the same page. Emscripten compiled code +assumes that it will be the only emscripten module on the page at any given +time, so this was a bit challenging and involved editing the generated JS. + +The first thing I did was wrap all of the modules in isolated AMD loaders. You +can see how some of this ended up looking by visiting the actual scripts +(warning, big files): + +* [scas.js](http://localhost:4000/tools/scas.js) +* [kpack.js](http://localhost:4000/tools/kpack.js) +* [genkfs.js](http://localhost:4000/tools/genkfs.js) + +That was enough to make it so that they could all run. These are part of a +toolchain, though, so somehow they needed to share files. Emscripten's [FS +object](http://kripken.github.io/emscripten-site/docs/api_reference/Filesystem-API.html) +cannot be shared between modules, and their API for mounting filesystems is +pretty crappy. So the solution was to write a little JS: + +{% highlight coffeescript %} +copy_between_systems = (fs1, fs2, from, to, encoding) -> + for f in fs1.readdir(from) + continue if f in ['.', '..'] + fs1p = from + '/' + f + fs2p = to + '/' + f + s = fs1.stat(fs1p) + log("Writing #{fs1p} to #{fs2p}") + if fs1.isDir(s.mode) + try + fs2.mkdir(fs2p) + catch + # pass + copy_between_systems(fs1, fs2, fs1p, fs2p, encoding) + else + fs2.writeFile(fs2p, fs1.readFile(fs1p, { encoding: encoding }), { encoding: encoding }) +{% endhighlight %} + +With this, we can extract packages in the kpack filesystem and copy them to the +genkfs filesystem: + +{% highlight coffeescript %} +install_package = (repo, name, callback) -> + full_name = repo + '/' + name + log("Downloading " + full_name) + xhr = new XMLHttpRequest() + xhr.open('GET', "https://packages.knightos.org/" + full_name + "/download") + xhr.responseType = 'arraybuffer' + xhr.onload = () -> + log("Installing " + full_name) + file_name = '/packages/' + repo + '-' + name + '.pkg' + data = new Uint8Array(xhr.response) + toolchain.kpack.FS.writeFile(file_name, data, { encoding: 'binary' }) + toolchain.kpack.Module.callMain(['-e', file_name, '/pkgroot']) + copy_between_systems(toolchain.kpack.FS, toolchain.scas.FS, "/pkgroot/include", "/include", "utf8") + copy_between_systems(toolchain.kpack.FS, toolchain.genkfs.FS, "/pkgroot", "/root", "binary") + log("Package installed.") + callback() if callback? + xhr.send() +{% endhighlight %} + +And this puts all the pieces in place for us to actually pass an assembly file +through our toolchain: + +{% highlight coffeescript %} +run_project = (main) -> + # Assemble + window.toolchain.scas.FS.writeFile('/main.asm', main) + log("Calling assembler...") + ret = window.toolchain.scas.Module.callMain(['/main.asm', '-I/include/', '-o', 'executable']) + return ret if ret != 0 + log("Assembly done!") + # Build filesystem + executable = window.toolchain.scas.FS.readFile("/executable", { encoding: 'binary' }) + window.toolchain.genkfs.FS.writeFile("/root/bin/executable", executable, { encoding: 'binary' }) + window.toolchain.genkfs.FS.writeFile("/root/etc/inittab", "/bin/executable") + window.toolchain.genkfs.FS.writeFile("/kernel.rom", new Uint8Array(toolchain.kernel_rom), { encoding: 'binary' }) + window.toolchain.genkfs.Module.callMain(["/kernel.rom", "/root"]) + rom = window.toolchain.genkfs.FS.readFile("/kernel.rom", { encoding: 'binary' }) + + log("Loading your program into the emulator!") + if current_emulator != null + current_emulator.cleanup() + current_emulator = new toolchain.ide_emu(document.getElementById('screen')) + current_emulator.load_rom(rom.buffer) + return 0 +{% endhighlight %} + +This was fairly easy to put together once we got all the tools to cooperate. +After all, these are all command-line tools. Invoking them is as simple as +calling `main` and then fiddling with the files that come out. Porting z80e, on +the other hand, was not nearly as simple. + +## Porting z80e to the browser + +[z80e](https://github.com/KnightOS/z80e) is our calculator emulator. It's also +written in C, but needs to interact much more closely with the user. We need to +be able to render the display to a canvas, and to receive input from the user. +This isn't nearly as simple as just calling `main` and playing with some files. + +To accomplish this, we've put together +[OpenTI](https://github.com/KnightOS/OpenTI), a set of JavaScript bindings to +z80e. This is mostly the work of my friend puckipedia, but I can explain a bit +of what is involved. The short of it is that we needed to map native structs to +JavaScript objects and pass JavaScript code as function pointers to z80e's +hooks. So far as I know, the KnightOS team is the only group to have attempted +something with this deep of integration between emscripten and JavaScript - +because we had to do a ton of the work ourselves. + +OpenTI contains a +[wrap](https://github.com/KnightOS/OpenTI/blob/master/webui/js/OpenTI/wrap.js) +module that is capable of wrapping structs and pointers in JavaScript objects. +This is a tedious procedure, because we have to know the offset and size of each +field in native code. An example of a wrapped object is given here: + +{% highlight javascript %} +define(["../wrap"], function(Wrap) { + var Registers = function(pointer) { + if (!pointer) { + throw "This object can only be instantiated with a memory region predefined!"; + } + this.pointer = pointer; + + Wrap.UInt16(this, "AF", pointer); + Wrap.UInt8(this, "F", pointer); + Wrap.UInt8(this, "A", pointer + 1); + + this.flags = {}; + Wrap.UInt8(this.flags, "C", pointer, 128, 7); + Wrap.UInt8(this.flags, "N", pointer, 64, 6); + Wrap.UInt8(this.flags, "PV", pointer, 32, 5); + Wrap.UInt8(this.flags, "3", pointer, 16, 4); + Wrap.UInt8(this.flags, "H", pointer, 8, 3); + Wrap.UInt8(this.flags, "5", pointer, 4, 2); + Wrap.UInt8(this.flags, "Z", pointer, 2, 1); + Wrap.UInt8(this.flags, "S", pointer, 1, 0); + pointer += 2; + + Wrap.UInt16(this, "BC", pointer); + Wrap.UInt8(this, "C", pointer); + Wrap.UInt8(this, "B", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "DE", pointer); + Wrap.UInt8(this, "E", pointer); + Wrap.UInt8(this, "D", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "HL", pointer); + Wrap.UInt8(this, "L", pointer); + Wrap.UInt8(this, "H", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "_AF", pointer); + Wrap.UInt16(this, "_BC", pointer + 2); + Wrap.UInt16(this, "_DE", pointer + 4); + Wrap.UInt16(this, "_HL", pointer + 6); + pointer += 8; + + Wrap.UInt16(this, "PC", pointer); + Wrap.UInt16(this, "SP", pointer + 2); + pointer += 4; + + Wrap.UInt16(this, "IX", pointer); + Wrap.UInt8(this, "IXL", pointer); + Wrap.UInt8(this, "IXH", pointer + 1); + pointer += 2; + + Wrap.UInt16(this, "IY", pointer); + Wrap.UInt8(this, "IYL", pointer); + Wrap.UInt8(this, "IYH", pointer + 1); + pointer += 2; + + Wrap.UInt8(this, "I", pointer++); + Wrap.UInt8(this, "R", pointer++); + + // 2 dummy bytes needed for 4-byte alignment + } + + Registers.sizeOf = function() { + return 26; + } + + return Registers; +}); +{% endhighlight %} + +The result of that effort is that you can find out what the current value of a +register is from some nice clean JavaScript: `asic.cpu.registers.PC` (it's <code +id="register-pc"></code>, by the way). + +## Conclusions + +I've put all of this together on [try.knightos.org](http://try.knightos.org). +The source is available on +[GitHub](https://github.com/KnightOS/try.knightos.org). It's entirely +client-side, so it can be hosted on GitHub Pages. I'm hopeful that this will +make it easier for people to get interested in KnightOS development, but it'll +be a lot better once I can get more documentation and tutorials written. It'd be +pretty cool if we could have interactive tutorials like this! + +It was a lot of effort to make this happen, but it was worth it. This is some +pretty cool shit we've got as a result. + +If you, reader, are interested in working on some pretty cool shit, there's a +place for you! We have things to do in Assembly, C, JavaScript, Python, and a +handful of other things. Did you notice how bad try.knightos.org looks? Maybe +you have a knack for design and want to help improve it. Whatever the case may +be, if you have interest in this stuff, come hang out with us on IRC: [#knightos +on irc.freenode.net](http://webchat.freenode.net/?channels=knightos&uio=d4). diff --git a/css/base.css b/css/base.css index e4996ab..b26c10f 100644 --- a/css/base.css +++ b/css/base.css @@ -34,3 +34,11 @@ pre { .footnotes a { color: #444; } + +blockquote { + padding-left: 10px; + padding-top: 3px; + padding-bottom: 3px; + margin-left: 0; + border-left: 4px #aaa solid; +} diff --git a/css/toolchain.scss b/css/toolchain.scss new file mode 100644 index 0000000..13ed29e --- /dev/null +++ b/css/toolchain.scss @@ -0,0 +1,40 @@ +--- +--- + +.editor { + height: 400px; + margin-bottom: 10px; + border: 1px solid #666; +} + +.calculator-wrapper { + border-bottom: 1px solid #888; + position: fixed; + bottom: 0; + left: 0; + z-index: 99999; + + .calculator { + background: url('http://try.knightos.org/static/skin.png') no-repeat; + background-size: 500px; + width: 500px; + height: 330px; + margin: 0 auto; + position: relative; + + canvas { + position: absolute; + left: 100px; + top: 105px; + width: 300px; + } + } +} + +.run-button { + position: absolute; + width: 100px; + top: 5px; + right: 15px; + z-index: 9999; +} diff --git a/js/ide_emu.js b/js/ide_emu.js new file mode 100644 index 0000000..252e28e --- /dev/null +++ b/js/ide_emu.js @@ -0,0 +1,150 @@ +define(['z80e', '../OpenTI/webui/js/OpenTI/OpenTI'], function(z80e, OpenTI) { + var lcd_ctx; + + var update_lcd; + function do_update_lcd(lcd) { + update_lcd = lcd; + } + + var lcd_data = []; + var lcd_colors = [[0x99, 0xB1, 0x99], [0x00, 0x00, 0x00]]; + function gen_pixeldata() { + for (var i = 0; i <= 0xff; i++) { + var arr = new Uint8Array(8 * 4 * 4); + for (var j = 0; j < 8; j++) { + var set = (i & (1 << j)) ? 1 : 0; + for (var k = 0; k < 4; k++) { + var view = (j * 16) + k * 4; + arr[view + 0] = lcd_colors[set][0]; + arr[view + 1] = lcd_colors[set][1]; + arr[view + 2] = lcd_colors[set][2]; + arr[view + 3] = 0xFF; + } + } + lcd_data.push(arr); + } + } + gen_pixeldata(); + + function print_lcd(lcd) { + var data = lcd_ctx.getImageData(0, 0, 384, 256); + var ram = lcd.ram; + for (var x = 0; x < (120 * 64) / 8; x++) { + var octet = x % 15; + if (octet > 11) { + continue; + } + var line = Math.floor(x / 15) * 4; + var tocopy = lcd_data[ram[x]]; + data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4)); + data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4)); + data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4)); + data.data.set(tocopy, ((line++) * 12 + octet) * (4 * 8 * 4)); + } + lcd_ctx.putImageData(data, 0, 0); + update_lcd = null; + } + + var key_mappings = Array.apply(null, new Array(100)).map(Number.prototype.valueOf, -1); + key_mappings[40] = 0x00; // Down + key_mappings[37] = 0x01; // Left + key_mappings[39] = 0x02; // Right + key_mappings[38] = 0x03; // Up + key_mappings[16] = 0x65; // 2nd / Shift + key_mappings[13] = 0x10; // Enter + key_mappings[27] = 0x66; // MODE / Esc + key_mappings[112] = 0x64; // F1 + key_mappings[113] = 0x63; // F2 + key_mappings[114] = 0x62; // F3 + key_mappings[115] = 0x61; // F4 + key_mappings[116] = 0x60; // F5 + key_mappings[48] = 0x40; // 0 + key_mappings[49] = 0x41; // 1 + key_mappings[50] = 0x31; // 2 + key_mappings[51] = 0x21; // 3 + key_mappings[52] = 0x42; // 4 + key_mappings[53] = 0x32; // 5 + key_mappings[54] = 0x22; // 6 + key_mappings[55] = 0x43; // 7 + key_mappings[56] = 0x33; // 8 + key_mappings[57] = 0x23; // 9 + + return function(canvas, ide_log) { + var self = this; + lcd_ctx = canvas.getContext('2d'); + this.asic = new OpenTI.TI.ASIC(OpenTI.TI.DeviceType.TI84pSE); + this.asic.debugger = new OpenTI.Debugger.Debugger(this.asic); + this.asic.hook.addLCDUpdate(do_update_lcd); + this.keysEnabled = false; + window.current_asic = this.asic; + window.addEventListener('click', function(e) { + self.keysEnabled = e.target.tagName == 'CANVAS'; + }); + window.addEventListener('keydown', function(e) { + if (!self.keysEnabled) return; + if (e.keyCode <= key_mappings.length && key_mappings[e.keyCode] !== -1) { + e.preventDefault(); + self.asic.hardware.Keyboard.press(key_mappings[e.keyCode]); + } + }); + window.addEventListener('keyup', function(e) { + if (!self.keysEnabled) return; + if (e.keyCode <= key_mappings.length && key_mappings[e.keyCode] !== -1) { + e.preventDefault(); + self.asic.hardware.Keyboard.release(key_mappings[e.keyCode]); + } + }); + + var asic_tick, lcd_tick; + + this.exec = function exec(str) { + ide_log("z80e > " + str + "\n"); + + if (str.length == 0) { + str = prev_command; + } + prev_command = str; + + var state = new oti.Debugger.Debugger.State(asic.debugger, + { + print: function(str) { ide_log(str); }, + new_state: function() { return this; }, + closed: function() { } + }); + + state.exec(str); + } + + this.cleanup = function cleanup() { + clearTimeout(asic_tick); + clearTimeout(lcd_tick); + lcd_ctx.clearRect(0, 0, 385, 256); + return; + /* TODO: this causes assertion errors */ + self.asic.free(); + }; + + this.load_rom = function load_rom(arrayBuffer) { + var byteArray = new Uint8Array(arrayBuffer); + var pointer = z80e.Module.allocate(byteArray, 'i8', z80e.Module.ALLOC_STACK); + z80e.Module.HEAPU32[this.asic.mmu._flashPointer] = pointer; + + this.asic.runloop.tick(1000); + this.asic.cpu.halted = 0; + + asic_tick = setTimeout(function tick() { + if (!self.asic.stopped || self.asic.cpu.interrupt) { + self.asic.runloop.tick(self.asic.clock_rate / 20); + } + setTimeout(tick, 0); + }, 1000 / 60); + + lcd_tick = setTimeout(function tick() { + if (update_lcd) { + print_lcd(update_lcd); + } + setTimeout(tick, 1000 / 60); + }, 1000 / 60); + } + } +}) diff --git a/js/toolchain.coffee b/js/toolchain.coffee new file mode 100644 index 0000000..8f5a8a1 --- /dev/null +++ b/js/toolchain.coffee @@ -0,0 +1,244 @@ +--- +--- + +require.config({ + paths: { + 'z80e': '../tools/z80e' + }, + shim: { + '../tools/kpack': { + exports: 'exports' + }, + '../tools/genkfs': { + exports: 'exports' + }, + '../tools/scas': { + exports: 'exports' + }, + 'z80e': { + exports: 'exports' + } + } +}) + +window.toolchain = { + kpack: null, + genkfs: null, + scas: null, + z80e: null, + ide_emu: null, + kernel_rom: null, +} + +files = [] + +log_el = document.getElementById('tool-log') +log = (text) -> + console.log(text) + if log_el.innerHTML == '' + log_el.innerHTML += text + else + log_el.innerHTML += '\n' + text + log_el.scrollTop = log_el.scrollHeight +window.ide_log = log + +copy_between_systems = (fs1, fs2, from, to, encoding) -> + for f in fs1.readdir(from) + continue if f in ['.', '..'] + fs1p = from + '/' + f + fs2p = to + '/' + f + s = fs1.stat(fs1p) + log("Writing #{fs1p} to #{fs2p}") + if fs1.isDir(s.mode) + try + fs2.mkdir(fs2p) + catch + # pass + copy_between_systems(fs1, fs2, fs1p, fs2p, encoding) + else + fs2.writeFile(fs2p, fs1.readFile(fs1p, { encoding: encoding }), { encoding: encoding }) + +install_package = (repo, name, callback) -> + full_name = repo + '/' + name + log("Downloading " + full_name) + xhr = new XMLHttpRequest() + xhr.open('GET', "https://packages.knightos.org/" + full_name + "/download") + xhr.responseType = 'arraybuffer' + xhr.onload = () -> + log("Installing " + full_name) + file_name = '/packages/' + repo + '-' + name + '.pkg' + data = new Uint8Array(xhr.response) + toolchain.kpack.FS.writeFile(file_name, data, { encoding: 'binary' }) + toolchain.kpack.Module.callMain(['-e', file_name, '/pkgroot']) + copy_between_systems(toolchain.kpack.FS, toolchain.scas.FS, "/pkgroot/include", "/include", "utf8") + copy_between_systems(toolchain.kpack.FS, toolchain.genkfs.FS, "/pkgroot", "/root", "binary") + log("Package installed.") + callback() if callback? + xhr.send() + +current_emulator = null + +load_environment = -> + toolchain.genkfs.FS.writeFile("/kernel.rom", toolchain.kernel_rom, { encoding: 'binary' }) + toolchain.genkfs.FS.mkdir("/root") + toolchain.genkfs.FS.mkdir("/root/bin") + toolchain.genkfs.FS.mkdir("/root/etc") + toolchain.genkfs.FS.mkdir("/root/home") + toolchain.genkfs.FS.mkdir("/root/lib") + toolchain.genkfs.FS.mkdir("/root/share") + toolchain.genkfs.FS.mkdir("/root/var") + toolchain.kpack.FS.mkdir("/packages") + toolchain.kpack.FS.mkdir("/pkgroot") + toolchain.kpack.FS.mkdir("/pkgroot/include") + toolchain.scas.FS.mkdir("/include") + packages = 0 + callback = () -> + packages++ + log("Ready to go!") if packages == 2 + install_package('core', 'init', callback) + install_package('core', 'kernel-headers', callback) + +run_project = (main) -> + # Assemble + window.toolchain.scas.FS.writeFile('/main.asm', main) + log("Calling assembler...") + ret = window.toolchain.scas.Module.callMain(['/main.asm', '-I/include/', '-o', 'executable']) + return ret if ret != 0 + log("Assembly done!") + # Build filesystem + executable = window.toolchain.scas.FS.readFile("/executable", { encoding: 'binary' }) + window.toolchain.genkfs.FS.writeFile("/root/bin/executable", executable, { encoding: 'binary' }) + window.toolchain.genkfs.FS.writeFile("/root/etc/inittab", "/bin/executable") + window.toolchain.genkfs.FS.writeFile("/kernel.rom", new Uint8Array(toolchain.kernel_rom), { encoding: 'binary' }) + window.toolchain.genkfs.Module.callMain(["/kernel.rom", "/root"]) + rom = window.toolchain.genkfs.FS.readFile("/kernel.rom", { encoding: 'binary' }) + + log("Loading your program into the emulator!") + if current_emulator != null + current_emulator.cleanup() + current_emulator = new toolchain.ide_emu(document.getElementById('screen')) + current_emulator.load_rom(rom.buffer) + return 0 + +check_resources = -> + for prop in Object.keys(window.toolchain) + if window.toolchain[prop] == null + return + log("Ready.") + load_environment() + +downloadKernel = -> + log("Finding latest kernel on GitHub...") + xhr = new XMLHttpRequest() + xhr.open('GET', 'https://api.github.com/repos/KnightOS/kernel/releases') + xhr.onload = -> + json = JSON.parse(xhr.responseText) + release = json[0] + rom = new XMLHttpRequest() + if release? + log("Downloading kernel #{ release.tag_name }...") + rom.open('GET', _.find(release.assets, (a) -> a.name == 'kernel-TI84pSE.rom').url) + else + # fallback + log("Downloading kernel") + rom.open('GET', 'http://builds.knightos.org/latest-TI84pSE.rom') + rom.setRequestHeader("Accept", "application/octet-stream") + rom.responseType = 'arraybuffer' + rom.onload = () -> + window.toolchain.kernel_rom = rom.response + log("Loaded kernel ROM.") + check_resources() + rom.send() + xhr.onerror = -> + xhr.send() + +downloadKernel() + +log("Downloading scas...") +require(['../tools/scas'], (scas) -> + log("Loaded scas.") + window.toolchain.scas = scas + window.toolchain.scas.Module.preRun.pop()() + check_resources() +) + +log("Downloading kpack...") +require(['../tools/kpack'], (kpack) -> + log("Loaded kpack.") + window.toolchain.kpack = kpack + check_resources() +) + +log("Downloading genkfs...") +require(['../tools/genkfs'], (genkfs) -> + log("Loaded genkfs.") + window.toolchain.genkfs = genkfs + check_resources() +) + +log("Downloading emulator bindings...") +require(['ide_emu'], (ide_emu) -> + log("Loaded emulator bindings.") + window.toolchain.ide_emu = ide_emu + window.toolchain.z80e = require("z80e") + check_resources() +) + +((el) -> + # Set up default editors + editor = ace.edit(el) + editor.setTheme("ace/theme/github") + if el.dataset.file.indexOf('.asm') == el.dataset.file.length - 4 + editor.getSession().setMode("ace/mode/assembly_x86") + files.push({ + name: el.dataset.file, + editor: editor + }) + xhr = new XMLHttpRequest() + xhr.open('GET', el.dataset.source) + xhr.onload = () -> + editor.setValue(this.responseText) + editor.navigateFileStart() + xhr.send() + button = document.createElement('button') + button.className = 'run-button' + button.addEventListener('click', (e) -> + e.preventDefault() + ret = run_project(editor.getValue()) + if ret != 0 + alert("Assembler returned nonzero exit status, see log to the left") + ) + button.textContent = 'Run' + el.appendChild(button) +)(el) for el in document.querySelectorAll('.editor') + +document.getElementById('install-package').addEventListener('click', (e) -> + e.preventDefault() + p = document.getElementById('package-name').value.split('/') + install_package(p[0], p[1]) +) + +((el) -> + el.addEventListener('click', () -> + p = el.dataset.package.split('/') + install_package(p[0], p[1]) + ) +)(el) for el in document.querySelectorAll('.install-package-button') + +document.getElementById('hide-toolchain').addEventListener('click', (e) -> + e.preventDefault() + to = document.getElementById('tool-log') + if e.target.textContent == 'Hide toolchain output' + to.style.display = 'none' + e.target.textContent = 'Show toolchain output' + else + to.style.display = 'block' + e.target.textContent = 'Hide toolchain output' +) + +window.setInterval(() -> + if window.current_asic? + document.getElementById('register-pc').textContent = '0x' + window.current_asic.cpu.registers.PC.toString(16).toUpperCase() + else + document.getElementById('register-pc').textContent = '0x0000' +, 100) diff --git a/scas.data b/scas.data new file mode 100644 index 0000000..e2dd484 --- /dev/null +++ b/scas.data @@ -0,0 +1,533 @@ +# z80 Instruction Table + +#### INSTRUCTION +# INS [MNOMIC] [VALUE] +# MNOMIC is any series of case-insenstive characters with support for special +# characters to define additional functionality. MNOMIC may not have whitespace. +# Special Characters: +# '_': Required whitespace +# '-': Optional whitespace +# '%#<bits[s]>': Immediate value (# is a character to use to identify later) +# '^#<bits[s]>': Immediate value relative to PC (# is a character to use to identify later) +# '@#<group>': Operand (# is a ch |