{"id":38462552,"url":"https://github.com/stever/zxcode","last_synced_at":"2026-01-17T05:00:15.300Z","repository":{"id":48025489,"uuid":"451920728","full_name":"stever/zxcode","owner":"stever","description":"ZX Spectrum emulator \u0026 programming environment for the web-browser.","archived":false,"fork":false,"pushed_at":"2025-10-05T16:56:15.000Z","size":5878,"stargazers_count":13,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-10-05T17:41:28.878Z","etag":null,"topics":["codespaces","zxspectrum"],"latest_commit_sha":null,"homepage":"https://code.zxplay.org","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stever.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["stever"]}},"created_at":"2022-01-25T14:57:15.000Z","updated_at":"2025-10-05T16:56:18.000Z","dependencies_parsed_at":"2023-02-09T23:31:06.146Z","dependency_job_id":"cb2975d6-4ad0-4ba0-9061-2269c1f3c8ff","html_url":"https://github.com/stever/zxcode","commit_stats":null,"previous_names":["zxplay/zxcoder","stever/zxcoder","stever/zxcode"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stever/zxcode","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stever%2Fzxcode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stever%2Fzxcode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stever%2Fzxcode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stever%2Fzxcode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stever","download_url":"https://codeload.github.com/stever/zxcode/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stever%2Fzxcode/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28497943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T04:31:57.058Z","status":"ssl_error","status_checked_at":"2026-01-17T04:31:45.816Z","response_time":85,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["codespaces","zxspectrum"],"created_at":"2026-01-17T05:00:10.806Z","updated_at":"2026-01-17T05:00:15.197Z","avatar_url":"https://github.com/stever.png","language":"JavaScript","readme":"# Code · ZX Play\n\nA ZX Spectrum emulator \u0026 programming environment for the browser.\n\n## Development Notes\n\n### Local Development\n\nMinimum pre-requisites:\n\n- Docker with Docker Compose (https://docs.docker.com/get-docker/)\n- Caddy web server (https://caddyserver.com/) caddy command installed and in PATH\n- Microsoft .NET 8 SDK (https://dotnet.microsoft.com/en-us/download/dotnet/8.0)\n\n```bash\n# Create .env files from example .env-dist files\ncp .env-dist .env\ncp apps/proxy/.env-dist apps/proxy/.env\n\n# Set env vars before starting containers\nexport HASURA_GRAPHQL_ADMIN_SECRET=hasurapassword\n\n# Start up containers\ndocker compose up --build -d\n\n# Wait for Hasura to start\nbash -c 'while [[ \"$(curl -s -o /dev/null -w ''%{http_code}'' localhost:4000/healthz)\" != \"200\" ]]; do sleep 5; done'\n```\n\n```bash\nnpm install\nnpm run dev\n```\n\nLaunch the URL for the proxied web server on port 8080 (http://localhost:8080).\n\n### Docker Commands\n\nRemove docker compose deployment to start over:\n\n```bash\ndocker compose stop \u0026\u0026 docker compose rm -f\ndocker volume rm zxcoder_pg_data\n```\n\nRefresh and restart docker-compose deployment:\n\n```bash\ndocker compose pull\ndocker compose up --build -d\n```\n\n### HTTP Local Ports Used\n\n| Port | Purpose        | Protocol |\n| ---- | -------------- | -------- |\n| 4000 | Hasura GraphQL | HTTP     |\n| 5000 | Auth           | HTTP     |\n| 8000 | React          | HTTP     |\n| 8080 | Proxy          | HTTP     |\n\n## JSSpeccy3 Core\n\nBased on [JSSpeccy3](https://github.com/gasman/jsspeccy3).\n\nThis is a personal fork for some customisation also using mods from other forks.\n\n### Features\n\n- Emulates the Spectrum 48K, Spectrum 128K and Pentagon machines\n- Handles all Z80 instructions, documented and undocumented\n- Cycle-accurate emulation of scanline / multicolour effects\n- AY and beeper audio\n- Loads SZX, Z80 and SNA snapshots\n- Loads TZX and TAP tape images (via traps only)\n- Loads any of the above files from inside a ZIP file\n- 100% / 200% / 300% and fullscreen display modes\n\n### Implementation notes\n\nJSSpeccy 3 is a complete rewrite of JSSpeccy to make full use of the web technologies\nand APIs available as of 2021 for high-performance web apps. The emulation runs in a\nWeb Worker, freeing up the UI thread to handle screen and audio updates, with the\nemulator core (consisting of the Z80 processor emulation and any auxiliary processes\nthat are likely to interrupt its execution multiple times per frame, such as constructing\nthe video output, reading the keyboard and generating audio) running in WebAssembly,\ncompiled from AssemblyScript (with a custom preprocessor).\n\n### JSSpeccy 3 mobile mod\n\nSource: https://github.com/dcrespo3d/jsspeccy3-mobile\n\nDesigned for mobile browsers with per-game customizable soft keys.\n\nConfigured using URL parameters.\n\n#### Example URL\n\nhttps://code.zxplay.org/?k=-W-P,ASDe,123456789M\u0026m=48\u0026u=https://davidprograma.github.io/ytc/09-ZxSpectrum/snake-1.01.tap\n\nThe URL can be decomposed in these parts:\n\n- Main part:\n\n```\nhttps://code.zxplay.org/\n```\n\n- Soft keys:\n\n```\n?k=-W-P,ASDe,123456789M\n```\n\n- Machine type (48, 128, 5 for pentagon)\n\n```\n\u0026m=48\n```\n\n- Program/game URL to load:\n\n```\n\u0026u=https://davidprograma.github.io/ytc/09-ZxSpectrum/snake-1.01.tap\n```\n\nYou can build your own URL setting soft keys, machine type (defaults to 48) and a\nURL (\\*) for a Z80, SNA, SZX, TZX or TAP file containing the desired game or program.\n\n(\\*) Target URL **must** be hosted in a website with CORS enabled.\n\n- Optional filtering (default 0 is not filtered, good old pixels):\n\n```\n\u0026f=1\n```\n\n#### Soft key syntax\n\nThe syntax is simple: keys are arranged as rows, and rows are separated by commas.\nSo, the previous strings has 3 rows:\n\n```\n-W-P\nASDe\n123456789M\n```\n\nA key is defined by its UPPERCASE character, and a hyphen (-) means a blank.\n\nThe exceptions are:\n\n- Enter key: e (lowercase e)\n- Caps shift: c (lowercase c)\n- Symbol shift: s (lowercase s)\n- Space: \\_ (underscore)\n\n### Palette mod\n\nSource: https://github.com/cronomantic/jsspeccy3\n\nThe RGB values described [here](https://en.wikipedia.org/wiki/ZX_Spectrum_graphic_modes#Colour_palette)\nare most likely calculated by measuring voltages on the RGB output of the 128k models, since the ULAs of\nthose systems generate RGBI signals that later are encoded to composite by the TEA2000 IC.\nThose are the colors that most emulators use and most people are used to.\n\n### Tech notes\n\n#### Architecture\n\nThe browser UI thread (starting point in runtime/jsspeccy.js) is kept as lightweight\nas possible, only performing tasks that are directly related to communication with\nthe \"outside world\": rendering the screen data to a canvas, handling keyboard events,\noutputting audio and managing UI actions such as loading files.\n\nAll the actual emulation happens inside a Web Worker (runtime/worker.js), with all\ncommuncation between the UI thread and the worker happening through `postMessage`.\nThe most important messages are `runFrame` (sent from the UI thread to the worker,\nto tell it to run one frame of emulation and fill the passed video and audio buffers\nwith the resulting output) and `frameCompleted` (sent from the worker to the UI thread\nwhen execution of the frame is complete, passing the filled video and audio buffers back).\n\nWithin the Web Worker, all of the performance-critical work is handled by a WebAssembly\nmodule (jsspeccy-core.wasm). The main entry point into this is the `runFrame` function,\nwhich runs the Z80 and all related 'continuous' processes (memory reads / writes,\nresponding to port reads / writes, building the screen and generating audio) for one\nvideo frame. `runFrame` returns a status of 1 to indicate that the frame has completed\nexecution (and thus the video / audio buffers are ready to send back to the UI thread),\nwith other status values serving as 'exceptions', indicating that execution was\ninterrupted and needs action from the calling code before it can be continued (by\ncalling `resumeFrame`). At the time of writing, the only kind of exception implemented\nis a tape loading trap.\n\nAll state required for the WebAssembly core module to run - including memory\ncontents (ROM and RAM), registers, audio / video buffers and lookup tables - is\ncontained within the module's own memory map, and statically allocated at compile time.\n\nOn the real machine, generating video and audio output happens in parallel with\nthe Z80's execution - an emulator implementing this naïvely would have to break\nout of the Z80 loop every few cycles to perform these tasks. In fact, these\nprocesses can be deferred for as long as we like, as long as we catch up on them\nbefore any state changes occur that would affect the output. With this in mind,\nthe JSSpeccy core implements two functions `updateFramebuffer` and `updateAudioBuffer`\nwhich perform all pending video / audio generation as far as the current Z80 cycle.\nThese are called immediately before any state change (which means, for audio, a\nwrite to any AY register or the beeper port; and for video, a write to video memory,\nchange of border colour or a write to the memory paging port).\n\n#### Building the core\n\nTo build jsspeccy-core.wasm, we run the script generator/gencore.mjs, which runs\na preprocessing pass over the input file generator/core.ts.in, to generate the\n[AssemblyScript](https://www.assemblyscript.org/) source file build/core.ts. This\nis then passed to the AssemblyScript compiler to produce the final dist/jsspeccy-core.wasm module.\n\nThe preprocessor step serves two purposes: firstly, it allows us to programmatically\nbuild the large repetitive `switch` statements that form the Z80 core. Secondly,\nit allows us to use conventional array syntax to access our statically-defined arrays.\nCurrently, AssemblyScript does not appear to have any native support for static\narrays - any use of array syntax causes it to immediately pull in a `malloc`\nimplementation and a higher-level array construct with bounds checking, all of which\nis unwanted overhead for our purposes. The gencore.mjs processor rewrites array\nsyntax into direct memory access [`load` / `store` instructions](https://www.assemblyscript.org/stdlib/builtins.html#memory).\n\nAll statically-defined arrays are allocated at the start of the module's memory map,\nfrom address 0 onward. Currently a 512Kb block is allocated for these - if you need\nmore, increase `memoryBase` in asconfig.json.\n\nThe gencore.mjs preprocessor recognises the following directives:\n\n- `#alloc` - allocates an array of the given size and type. For example, if `#alloc frameBuffer[0x6600]: u8` is the first line of the file, then 0x6600 bytes from address 0 will be allocated to an array named `frameBuffer`. This will then rewrite subsequent lines as follows:\n  - An assignment such as `frameBuffer = [0x00, 0x01, 0x02];` will be rewritten as a sequence of `store\u003cu8\u003e(0, 0x00);`, `store\u003cu8\u003e(1, 0x01);` lines\n  - An assignment such as `frameBuffer[ptr] = 0x00;` will be rewritten as `store\u003cu8\u003e(0 + ptr, 0x00);`\n  - A lookup such as `val = frameBuffer[ptr];` will be rewritten as `val = load\u003cu8\u003e(0 + ptr);`\n  - `(\u0026frameBuffer)` will be replaced with the array's base address, e.g. `const FRAME_BUFFER = (\u0026frameBuffer);` becomes `const FRAME_BUFFER = 0;`\n  - Keep in mind that these are simple regexp replacements, not a full parser - it's likely to fail on statements that are split over multiple lines, or have nested brackets. If you don't like this, feel free to submit a better implementation of static arrays to the AssemblyScript project :-)\n- `#const` - defines an identifier to be replaced by the given expression. For example, given a directive `#const FLAG_C 0x01`, a subsequent line `result \u0026= FLAG_C;` will be rewritten to `result \u0026= 0x01;`. `const FLAG_C = 0x01;` would achieve the same thing, but will also define a symbol in the resulting module, which we probably don't want.\n- `#regpair` - allocates two bytes to store a Z80 register pair. This is always little-endian, as per the WebAssembly spec. For example, if the next memory address to be allocated is 0x1000, then `#regpair BC B C` will define identifiers `BC`, `B` and `C` such that:\n  - `val = BC;` is rewritten to `val = load\u003cu16\u003e(0x1000);`\n  - `BC = 0x1234;` is rewritten to `store\u003cu16\u003e(0x1000, 0x1234);`\n  - `val = B;` is rewritten to `val = load\u003cu8\u003e(0x1001);`\n  - `B = result;` is rewritten to `store\u003cu8\u003e(0x1001, result);`\n  - `val = C;` is rewritten to `val = load\u003cu8\u003e(0x1000);`\n  - `C = result;` is rewritten to `store\u003cu8\u003e(0x1000, result);`\n- `#optable` - generates the sequence of `case` statements that decode an opcode byte. The subroutine bodies for each class of instruction are defined in generator/instructions.mjs, and these are pattern-matched to the actual instruction lists in generator/opcodes\\_\\*.txt.\n\n#### Frame buffer format\n\nThe frame buffer data structure (as written by the WebAssembly core and passed to\nthe UI thread in the `frameCompleted` message) is essentially a log of all border,\nscreen and attribute bytes in the order that they would be read to build the video\noutput. This is based on a 320x240 output image consisting of 24 lines of upper\nborder, 192 lines of main screen (each consisting of 32px left border, 256px main\nscreen, and 32px right border), and 24 lines of lower border. This results in a\n0x6600 byte buffer, breaking down as follows:\n\n- 0x0000..0x009f: line 0 of the upper border. 160 bytes, each one being a border colour (0..7) and contributing two pixels to the final image. (This corresponds to the maximum resolution at which border colour changes happen on the Pentagon; these take effect on every cycle, and one cycle equals two pixels.)\n- 0x00a0..0x013f: line 1 of the upper border\n- ...\n- 0x0e60..0x0eff: line 23 of the upper border\n- 0x0f00..0x0f0f: left border of main screen line 0. 16 bytes, each contributing two pixels of border as before\n- 0x0f10..0x0f4f: main screen line 0. 32\\*2 bytes, consisting of the pixel byte and attribute byte for each of the 32 character cells\n- 0x0f50..0x0f5f: right border of main screen line 0. 16 bytes, each contributing two pixels of border as before\n- 0x0f60..0x0f6f: left border of main screen line 1\n- 0x0f70..0x0faf: main screen line 1. (Again, since the data here is in the order that the video output would be generated, this is the data pulled from address 0x4100 onward, not 0x4020.)\n- 0x0fb0..0x0fbf: right border of main screen line 2\n- ...\n- 0x56a0..0x56af: left border of main screen line 191\n- 0x56b0..0x56ef: main screen line 191\n- 0x56f0..0x56ff: right border of main screen line 191\n- 0x5700..0x579f: line 0 of the lower border. 160 bytes, as per upper border\n- 0x57a0..0x583f: line 1 of the lower border\n- ...\n- 0x6560..0x65ff: line 23 of the lower border\n\n## Acknowledgements\n\nThis software uses code from the following open source projects:\n\n- JSSpeccy3 \u0026 JSSpeccy3-mobile. These are licensed under terms of the GPL version 3.\n- Pasmo by Julián Albo García, alias \"NotFound\". Licensed under terms of the GPL version 3.\n- Boriel ZX BASIC by Jose Rodriguez. Licensed under terms of the GPL version 3.\n- zmakebas by Russell Marks. This tool is public domain.\n- 8bitworkshop by Steven Hugg. Licensed under terms of the GPL version 3.\n","funding_links":["https://github.com/sponsors/stever"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstever%2Fzxcode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstever%2Fzxcode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstever%2Fzxcode/lists"}