Skip to content

Commit c83a7c5

Browse files
committed
Added zxplay.org as an app using shared emulator
1 parent 586efd3 commit c83a7c5

110 files changed

Lines changed: 11361 additions & 3922 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/play/.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
*.interp
2+
*.swp
3+
*.tokens
4+
/.graphqlconfig
5+
/.idea/
6+
/build/
7+
/es5/
8+
/node_modules/
9+
/public/dist/
10+
/schema.graphql
11+
/typedocs/

apps/play/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*.DS_Store
2+
*.swp
3+
.env
4+
.idea/
5+
build/
6+
es5/
7+
node_modules/
8+
public/dist/
9+
# Emulator runtime assets are copied in from @zxplay/emulator at build time.
10+
public/roms/
11+
public/tapeloaders/
12+
typedocs/

apps/play/COPYING

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

apps/play/Caddyfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:8080 {
2+
route {
3+
root * /srv
4+
try_files {path} {path}/ /index.html
5+
file_server
6+
}
7+
}

apps/play/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM caddy:2 AS base
2+
COPY Caddyfile /etc/caddy/Caddyfile
3+
4+
FROM node:19 AS npmbuild
5+
WORKDIR /project
6+
COPY . .
7+
RUN npm install --force
8+
RUN npm run build
9+
10+
FROM base AS final
11+
COPY --from=npmbuild /project/public /srv
12+
RUN sed -i "s|ver=0|"ver=`date +"%s"`"|g" /srv/index.html

apps/play/README.md

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# ZX Play
2+
3+
A mobile-friendly ZX Spectrum emulator for the browser.
4+
5+
## Fresh Start
6+
7+
```bash
8+
npm install
9+
npm run dev
10+
```
11+
12+
Launch the URL for the web server on port 8000 (http://localhost:8000).
13+
14+
## JSSpeccy3 Core
15+
16+
Based on [JSSpeccy3](https://github.com/gasman/jsspeccy3).
17+
18+
### Features
19+
20+
* Emulates the Spectrum 48K, Spectrum 128K and Pentagon machines
21+
* Handles all Z80 instructions, documented and undocumented
22+
* Cycle-accurate emulation of scanline / multicolour effects
23+
* AY and beeper audio
24+
* Loads SZX, Z80 and SNA snapshots
25+
* Loads TZX and TAP tape images (via traps only)
26+
* Loads any of the above files from inside a ZIP file
27+
* 100% / 200% / 300% and fullscreen display modes
28+
29+
### Implementation notes
30+
31+
JSSpeccy 3 is a complete rewrite of JSSpeccy to make full use of the web technologies
32+
and APIs available as of 2021 for high-performance web apps. The emulation runs in a
33+
Web Worker, freeing up the UI thread to handle screen and audio updates, with the
34+
emulator core (consisting of the Z80 processor emulation and any auxiliary processes
35+
that are likely to interrupt its execution multiple times per frame, such as constructing
36+
the video output, reading the keyboard and generating audio) running in WebAssembly,
37+
compiled from AssemblyScript (with a custom preprocessor).
38+
39+
### JSSpeccy 3 mobile mod
40+
41+
Source: https://github.com/dcrespo3d/jsspeccy3-mobile
42+
43+
Designed for mobile browsers with per-game customizable soft keys.
44+
45+
Configured using URL parameters.
46+
47+
#### Example URL
48+
49+
https://zxplay.org/?k=-W-P,ASDe,123456789M&m=48&u=https://davidprograma.github.io/ytc/09-ZxSpectrum/snake-1.01.tap
50+
51+
The URL can be decomposed in these parts:
52+
- Main part:
53+
```
54+
https://zxplay.org/
55+
```
56+
- Soft keys:
57+
```
58+
?k=-W-P,ASDe,123456789M
59+
```
60+
- Machine type (48, 128, 5 for pentagon)
61+
```
62+
&m=48
63+
```
64+
- Program/game URL to load:
65+
```
66+
&u=https://davidprograma.github.io/ytc/09-ZxSpectrum/snake-1.01.tap
67+
```
68+
69+
You can build your own URL setting soft keys, machine type (defaults to 48) and a
70+
URL (*) for a Z80, SNA, SZX, TZX or TAP file containing the desired game or program.
71+
72+
(*) Target URL **must** be hosted in a website with CORS enabled.
73+
74+
- Optional filtering (default 0 is not filtered, good old pixels):
75+
```
76+
&f=1
77+
```
78+
79+
#### Soft key syntax
80+
81+
The syntax is simple: keys are arranged as rows, and rows are separated by commas.
82+
So, the previous strings has 3 rows:
83+
84+
```
85+
-W-P
86+
ASDe
87+
123456789M
88+
```
89+
90+
A key is defined by its UPPERCASE character, and a hyphen (-) means a blank.
91+
92+
The exceptions are:
93+
- Enter key: e (lowercase e)
94+
- Caps shift: c (lowercase c)
95+
- Symbol shift: s (lowercase s)
96+
- Space: _ (underscore)
97+
98+
### Palette mod
99+
100+
Source: https://github.com/cronomantic/jsspeccy3
101+
102+
The RGB values described [here](https://en.wikipedia.org/wiki/ZX_Spectrum_graphic_modes#Colour_palette)
103+
are most likely calculated by measuring voltages on the RGB output of the 128k models, since the ULAs of
104+
those systems generate RGBI signals that later are encoded to composite by the TEA2000 IC.
105+
Those are the colors that most emulators use and most people are used to.
106+
107+
### Tech notes
108+
109+
#### Architecture
110+
111+
The browser UI thread (starting point in runtime/jsspeccy.js) is kept as lightweight
112+
as possible, only performing tasks that are directly related to communication with
113+
the "outside world": rendering the screen data to a canvas, handling keyboard events,
114+
outputting audio and managing UI actions such as loading files.
115+
116+
All the actual emulation happens inside a Web Worker (runtime/worker.js), with all
117+
communcation between the UI thread and the worker happening through `postMessage`.
118+
The most important messages are `runFrame` (sent from the UI thread to the worker,
119+
to tell it to run one frame of emulation and fill the passed video and audio buffers
120+
with the resulting output) and `frameCompleted` (sent from the worker to the UI thread
121+
when execution of the frame is complete, passing the filled video and audio buffers back).
122+
123+
Within the Web Worker, all of the performance-critical work is handled by a WebAssembly
124+
module (jsspeccy-core.wasm). The main entry point into this is the `runFrame` function,
125+
which runs the Z80 and all related 'continuous' processes (memory reads / writes,
126+
responding to port reads / writes, building the screen and generating audio) for one
127+
video frame. `runFrame` returns a status of 1 to indicate that the frame has completed
128+
execution (and thus the video / audio buffers are ready to send back to the UI thread),
129+
with other status values serving as 'exceptions', indicating that execution was
130+
interrupted and needs action from the calling code before it can be continued (by
131+
calling `resumeFrame`). At the time of writing, the only kind of exception implemented
132+
is a tape loading trap.
133+
134+
All state required for the WebAssembly core module to run - including memory
135+
contents (ROM and RAM), registers, audio / video buffers and lookup tables - is
136+
contained within the module's own memory map, and statically allocated at compile time.
137+
138+
On the real machine, generating video and audio output happens in parallel with
139+
the Z80's execution - an emulator implementing this naïvely would have to break
140+
out of the Z80 loop every few cycles to perform these tasks. In fact, these
141+
processes can be deferred for as long as we like, as long as we catch up on them
142+
before any state changes occur that would affect the output. With this in mind,
143+
the JSSpeccy core implements two functions `updateFramebuffer` and `updateAudioBuffer`
144+
which perform all pending video / audio generation as far as the current Z80 cycle.
145+
These are called immediately before any state change (which means, for audio, a
146+
write to any AY register or the beeper port; and for video, a write to video memory,
147+
change of border colour or a write to the memory paging port).
148+
149+
#### Building the core
150+
151+
To build jsspeccy-core.wasm, we run the script generator/gencore.mjs, which runs
152+
a preprocessing pass over the input file generator/core.ts.in, to generate the
153+
[AssemblyScript](https://www.assemblyscript.org/) source file build/core.ts. This
154+
is then passed to the AssemblyScript compiler to produce the final dist/jsspeccy-core.wasm module.
155+
156+
The preprocessor step serves two purposes: firstly, it allows us to programmatically
157+
build the large repetitive `switch` statements that form the Z80 core. Secondly,
158+
it allows us to use conventional array syntax to access our statically-defined arrays.
159+
Currently, AssemblyScript does not appear to have any native support for static
160+
arrays - any use of array syntax causes it to immediately pull in a `malloc`
161+
implementation and a higher-level array construct with bounds checking, all of which
162+
is unwanted overhead for our purposes. The gencore.mjs processor rewrites array
163+
syntax into direct memory access [`load` / `store` instructions](https://www.assemblyscript.org/stdlib/builtins.html#memory).
164+
165+
All statically-defined arrays are allocated at the start of the module's memory map,
166+
from address 0 onward. Currently a 512Kb block is allocated for these - if you need
167+
more, increase `memoryBase` in asconfig.json.
168+
169+
The gencore.mjs preprocessor recognises the following directives:
170+
171+
* `#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:
172+
* An assignment such as `frameBuffer = [0x00, 0x01, 0x02];` will be rewritten as a sequence of `store<u8>(0, 0x00);`, `store<u8>(1, 0x01);` lines
173+
* An assignment such as `frameBuffer[ptr] = 0x00;` will be rewritten as `store<u8>(0 + ptr, 0x00);`
174+
* A lookup such as `val = frameBuffer[ptr];` will be rewritten as `val = load<u8>(0 + ptr);`
175+
* `(&frameBuffer)` will be replaced with the array's base address, e.g. `const FRAME_BUFFER = (&frameBuffer);` becomes `const FRAME_BUFFER = 0;`
176+
* 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 :-)
177+
* `#const` - defines an identifier to be replaced by the given expression. For example, given a directive `#const FLAG_C 0x01`, a subsequent line `result &= FLAG_C;` will be rewritten to `result &= 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.
178+
* `#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:
179+
* `val = BC;` is rewritten to `val = load<u16>(0x1000);`
180+
* `BC = 0x1234;` is rewritten to `store<u16>(0x1000, 0x1234);`
181+
* `val = B;` is rewritten to `val = load<u8>(0x1001);`
182+
* `B = result;` is rewritten to `store<u8>(0x1001, result);`
183+
* `val = C;` is rewritten to `val = load<u8>(0x1000);`
184+
* `C = result;` is rewritten to `store<u8>(0x1000, result);`
185+
* `#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.
186+
187+
#### Frame buffer format
188+
189+
The frame buffer data structure (as written by the WebAssembly core and passed to
190+
the UI thread in the `frameCompleted` message) is essentially a log of all border,
191+
screen and attribute bytes in the order that they would be read to build the video
192+
output. This is based on a 320x240 output image consisting of 24 lines of upper
193+
border, 192 lines of main screen (each consisting of 32px left border, 256px main
194+
screen, and 32px right border), and 24 lines of lower border. This results in a
195+
0x6600 byte buffer, breaking down as follows:
196+
197+
* 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.)
198+
* 0x00a0..0x013f: line 1 of the upper border
199+
* ...
200+
* 0x0e60..0x0eff: line 23 of the upper border
201+
* 0x0f00..0x0f0f: left border of main screen line 0. 16 bytes, each contributing two pixels of border as before
202+
* 0x0f10..0x0f4f: main screen line 0. 32*2 bytes, consisting of the pixel byte and attribute byte for each of the 32 character cells
203+
* 0x0f50..0x0f5f: right border of main screen line 0. 16 bytes, each contributing two pixels of border as before
204+
* 0x0f60..0x0f6f: left border of main screen line 1
205+
* 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.)
206+
* 0x0fb0..0x0fbf: right border of main screen line 2
207+
* ...
208+
* 0x56a0..0x56af: left border of main screen line 191
209+
* 0x56b0..0x56ef: main screen line 191
210+
* 0x56f0..0x56ff: right border of main screen line 191
211+
* 0x5700..0x579f: line 0 of the lower border. 160 bytes, as per upper border
212+
* 0x57a0..0x583f: line 1 of the lower border
213+
* ...
214+
* 0x6560..0x65ff: line 23 of the lower border
215+
216+
## Acknowledgements
217+
218+
This software uses code from the following open source projects:
219+
220+
* JSSpeccy3 & JSSpeccy3-mobile. These are licensed under terms of the GPL version 3.
221+
* Pasmo by Julián Albo García, alias "NotFound". Licensed under terms of the GPL version 3.
222+
* Boriel ZX BASIC by Jose Rodriguez. Licensed under terms of the GPL version 3.
223+
* zmakebas by Russell Marks. This tool is public domain.

apps/play/package.json

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"name": "play",
3+
"private": true,
4+
"scripts": {
5+
"dev": "run-s build:clean:all build:emulator:debug serve",
6+
"test": "jest --passWithNoTests",
7+
"build": "npm run build:all:release",
8+
"build:all:debug": "run-s build:clean:all build:copyfiles build:babel build:emulator:debug build:js:debug build:clean",
9+
"build:all:release": "run-s build:clean:all build:copyfiles build:babel build:emulator:release build:js:release build:clean",
10+
"build:clean": "del-cli build es5",
11+
"build:clean:all": "del-cli build es5 public/dist public/roms public/tapeloaders",
12+
"build:copyfiles": "copyfiles \"src/**/*\" -u 1 es5",
13+
"build:babel": "babel src --out-dir es5",
14+
"build:emulator:debug": "run-s emulator:build:debug copy:emulator",
15+
"build:emulator:release": "run-s emulator:build:release copy:emulator",
16+
"emulator:build:debug": "npm run build:debug -w @zxplay/emulator",
17+
"emulator:build:release": "npm run build -w @zxplay/emulator",
18+
"copy:emulator": "run-s copy:emulator:dist copy:emulator:roms copy:emulator:tapeloaders",
19+
"copy:emulator:dist": "copyfiles -f \"../../packages/emulator/dist/*\" public/dist",
20+
"copy:emulator:roms": "copyfiles -f \"../../packages/emulator/roms/*\" public/roms",
21+
"copy:emulator:tapeloaders": "copyfiles -f \"../../packages/emulator/tapeloaders/*\" public/tapeloaders",
22+
"build:js:debug": "webpack",
23+
"build:js:release": "webpack --env production",
24+
"watch": "npm-watch",
25+
"serve": "npx webpack serve --env codespace=${CODESPACE_NAME} --env domain=${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
26+
},
27+
"dependencies": {
28+
"@lagunovsky/redux-react-router": "^4.5.0",
29+
"@zxplay/emulator": "*",
30+
"axios": "^1.12.2",
31+
"clsx": "^2.1.1",
32+
"file-dialog": "^0.0.8",
33+
"history": "^5.3.0",
34+
"jszip": "^3.10.1",
35+
"pako": "^2.1.0",
36+
"primeflex": "^3.3.1",
37+
"primeicons": "^6.0.1",
38+
"primereact": "^9.6.4",
39+
"query-string": "^9.3.1",
40+
"react": "^18.3.1",
41+
"react-dom": "^18.3.1",
42+
"react-redux": "^9.2.0",
43+
"react-router-dom": "^6.30.1",
44+
"react-titled": "^2.1.0",
45+
"react-transition-group": "^4.4.5",
46+
"redux": "^5.0.1",
47+
"redux-saga": "^1.3.0",
48+
"sass": "^1.93.2"
49+
},
50+
"devDependencies": {
51+
"@babel/cli": "^7.26.5",
52+
"@babel/core": "^7.26.5",
53+
"@babel/plugin-transform-runtime": "^7.26.5",
54+
"@babel/preset-env": "^7.26.5",
55+
"@babel/preset-react": "^7.26.3",
56+
"babel-jest": "^29.7.0",
57+
"babel-loader": "^9.2.1",
58+
"copyfiles": "^2.4.1",
59+
"css-loader": "^7.1.2",
60+
"del-cli": "^6.0.0",
61+
"esbuild": "^0.25.10",
62+
"jest": "^29.7.0",
63+
"jest-environment-jsdom": "^30.2.0",
64+
"jest-transform-stub": "^2.0.0",
65+
"npm-run-all": "^4.1.5",
66+
"npm-watch": "^0.13.0",
67+
"process": "^0.11.10",
68+
"prop-types": "^15.8.1",
69+
"react-test-renderer": "^18.3.1",
70+
"sass-loader": "^16.0.4",
71+
"style-loader": "^4.0.0",
72+
"svg-inline-loader": "^0.2.3",
73+
"webpack": "^5.97.3",
74+
"webpack-cli": "^5.1.4",
75+
"webpack-dev-server": "^5.2.0"
76+
},
77+
"watch": {
78+
"build:emulator:debug": {
79+
"patterns": [
80+
"../../packages/emulator/src"
81+
],
82+
"extensions": [
83+
"js",
84+
"mjs",
85+
"in",
86+
"txt",
87+
"svg"
88+
]
89+
}
90+
},
91+
"jest": {
92+
"testEnvironment": "jsdom",
93+
"transform": {
94+
"^.+\\.[t|j]sx?$": "babel-jest",
95+
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)(\\?inline)?$": "jest-transform-stub"
96+
},
97+
"globals": {
98+
"STAGING_ENV": "prod"
99+
}
100+
},
101+
"babel": {
102+
"presets": [
103+
"@babel/preset-env",
104+
"@babel/preset-react"
105+
],
106+
"plugins": [
107+
"@babel/plugin-transform-runtime"
108+
]
109+
}
110+
}
4.54 KB
Loading
4.45 KB
Loading
12.2 KB
Loading

0 commit comments

Comments
 (0)