blockyard 0.0.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +359 -1
- package/README.md +48 -27
- package/SECURITY.md +2 -2
- package/bin/blockyard.js +2 -1
- package/docs/API.md +17 -15
- package/docs/ARCHITECTURE.md +128 -10
- package/docs/CONFIGURATION.md +39 -30
- package/docs/DEFECTS.md +4 -1
- package/docs/GETTING-STARTED.md +18 -8
- package/docs/INSTALL.md +97 -37
- package/docs/MEASUREMENTS.md +147 -0
- package/docs/PLAN-SCORCHED-YARD.md +456 -0
- package/docs/PLAN-SKIES.md +142 -0
- package/docs/SECURITY-AUDIT-2026-09-16.md +647 -0
- package/docs/SECURITY.md +58 -22
- package/docs/TROUBLESHOOTING.md +44 -5
- package/docs/USER-GUIDE.md +505 -35
- package/package.json +4 -2
- package/public/404.html +1 -1
- package/public/css/app.css +393 -82
- package/public/donate-qr.png +0 -0
- package/public/index.html +353 -109
- package/public/js/agents.js +228 -51
- package/public/js/app.js +131 -16
- package/public/js/blockanoid.js +15 -7
- package/public/js/blockout.js +15 -7
- package/public/js/blockscene3d.js +230 -38
- package/public/js/charts.js +21 -21
- package/public/js/depthchart.js +31 -27
- package/public/js/details3d.js +1481 -73
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +7 -1
- package/public/js/livingsky.js +494 -0
- package/public/js/login.js +8 -2
- package/public/js/markets.js +46 -8
- package/public/js/mining.js +314 -36
- package/public/js/panels.js +41 -28
- package/public/js/pricechart.js +14 -13
- package/public/js/quake.js +20 -0
- package/public/js/safenext.js +14 -0
- package/public/js/scorched.js +1051 -0
- package/public/js/scorchedai.js +227 -0
- package/public/js/scorchedair.js +286 -0
- package/public/js/scorchedfx.js +376 -0
- package/public/js/scorchedshop.js +105 -0
- package/public/js/scorchedwind.js +69 -0
- package/public/js/scorchedyard.js +1338 -0
- package/public/js/settings.js +368 -100
- package/public/js/soundcard.js +459 -0
- package/public/js/tetrust.js +15 -6
- package/public/js/tetsound.js +35 -5
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/scripts/check.js +46 -0
- package/scripts/donate-qr.py +12 -9
- package/scripts/dos-bench.js +56 -0
- package/scripts/index-build.js +9 -2
- package/scripts/pool-map.js +152 -36
- package/scripts/setup.js +142 -22
- package/scripts/shots.mjs +27 -0
- package/scripts/smoke.sh +7 -6
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +4 -2
- package/server/auth/sessions.js +33 -13
- package/server/chain/blockfile.js +64 -5
- package/server/chain/index/build.js +451 -58
- package/server/chain/index/heights.js +29 -3
- package/server/chain/index/live.js +13 -7
- package/server/chain/index/rows.js +6 -1
- package/server/chain/index/store.js +28 -5
- package/server/chain/index/worker.js +23 -11
- package/server/collect/logparse.js +65 -18
- package/server/collect/markets.js +76 -7
- package/server/collect/mining.js +32 -0
- package/server/collect/monitor.js +54 -12
- package/server/collect/network.js +305 -0
- package/server/config.js +53 -22
- package/server/http/api.js +119 -18
- package/server/http/games.js +77 -0
- package/server/http/server.js +30 -5
- package/server/http/sse.js +53 -7
- package/server/main.js +66 -11
- package/server/rpc/allowlist.js +26 -0
- package/server/rpc/client.js +30 -2
- package/server/store/audit.js +6 -1
- package/server/store/history.js +19 -3
- package/server/store/ledger.js +15 -4
- package/server/tls/selfsigned.js +160 -0
- package/systemd/blockyard.service +41 -8
- package/docs/PRIVATE-LEADERBOARD.md +0 -230
- package/docs/STATE-2026-09-09.md +0 -200
package/docs/INSTALL.md
CHANGED
|
@@ -12,7 +12,7 @@ machines you choose. Every setting mentioned here is described in full in
|
|
|
12
12
|
- [6. Run it as a service](#6-run-it-as-a-service)
|
|
13
13
|
- [7. Decide who can reach it](#7-decide-who-can-reach-it)
|
|
14
14
|
- [8. Accounts (optional)](#8-accounts-optional)
|
|
15
|
-
- [9. HTTPS (
|
|
15
|
+
- [9. HTTPS (on by default)](#9-https-on-by-default)
|
|
16
16
|
- [10. Behind a reverse proxy (optional)](#10-behind-a-reverse-proxy-optional)
|
|
17
17
|
- [11. Updating](#11-updating)
|
|
18
18
|
- [12. Uninstalling](#12-uninstalling)
|
|
@@ -22,7 +22,7 @@ machines you choose. Every setting mentioned here is described in full in
|
|
|
22
22
|
|
|
23
23
|
| need | notes |
|
|
24
24
|
|---|---|
|
|
25
|
-
| **Node.js 22 or newer** | `node -v` must print `v22` or later. Older runtimes fail on syntax at start-up, which looks like a bug in the app. Install from [nodejs.org](https://nodejs.org), your distribution's backports, or a version manager such as `nvm`. |
|
|
25
|
+
| **Node.js 22.2 or newer** | `node -v` must print `v22.2` or later (the index build uses its built-in CRC-32). Older runtimes fail on syntax at start-up, which looks like a bug in the app. Install from [nodejs.org](https://nodejs.org), your distribution's backports, or a version manager such as `nvm`. |
|
|
26
26
|
| **Bitcoin Core 25.0 or later** | [Bitcoin Core](https://github.com/bitcoin/bitcoin) with `server=1` and `txindex=1`, **on the same machine** as BlockYard, which reads the node's block files for the explorer's address index. A node on another machine is not supported. 25.0 is where `getblock` verbosity 3, which the index follower uses, arrived; 29.1 is what the macOS install was done against. `coinstatsindex=1` is optional (without it the UTXO figures are blank and the node is not asked for them). Not a pruned node: the index needs every block file. |
|
|
27
27
|
| **RPC credentials** | Either read access to the node's cookie file (`<datadir>/<chain>/.cookie`, the usual case on the same machine) or an RPC user and password. |
|
|
28
28
|
| **macOS or Linux** | There is nothing to compile, and the server calls no platform-specific API (no `child_process`, no `/proc`, no `systemctl`). Developed on Linux; a real install has been done on macOS (Core 29.1). The test suite runs in CI on Ubuntu, macOS and Windows (Node 22 and 24), but on Windows nothing more than the suite has been tried. Only the *service* instructions in section 6 are Linux-specific (they use systemd); on macOS run it in a terminal, or write a `launchd` plist. The index store opens its files per lookup, so macOS's default limit of 256 open files is enough. |
|
|
@@ -70,8 +70,10 @@ while every page keeps working: the Overview's "What this panel cannot tell you"
|
|
|
70
70
|
progress (phase, files done, rows so far, an ETA that settles after the first few files), the
|
|
71
71
|
address page says the same in place of a history, and an event — which the browser shows as a
|
|
72
72
|
notification — marks the start, the finish and a failure. When it finishes the follower starts
|
|
73
|
-
on the spot, so address pages work without a restart. Stopping BlockYard stops the build
|
|
74
|
-
|
|
73
|
+
on the spot, so address pages work without a restart. Stopping BlockYard stops the build, and the
|
|
74
|
+
next start resumes it: the files already scanned and the buckets already sorted are kept, and
|
|
75
|
+
only what was not finished is read again (at most about a minute of scanning). Three keys on the
|
|
76
|
+
node entry control it:
|
|
75
77
|
|
|
76
78
|
| key | meaning |
|
|
77
79
|
|---|---|
|
|
@@ -146,9 +148,10 @@ Balances are checked against the node: 40 of 40 sampled addresses equal `scantxo
|
|
|
146
148
|
satoshi (`node scripts/index-benchmark.js` runs that check and the lookup timings against your
|
|
147
149
|
own build).
|
|
148
150
|
|
|
149
|
-
**Outbound network access
|
|
150
|
-
explorer's dollar figures (HTTPS to five
|
|
151
|
-
|
|
151
|
+
**Outbound network access**: none, out of the box — everything talks only to your node. The
|
|
152
|
+
Markets and Kiosk tabs and the explorer's dollar figures need the exchange feed (HTTPS to five
|
|
153
|
+
exchanges' public APIs), which is off until you tick **Display settings → Markets & Price → Enable market polling**
|
|
154
|
+
in the browser. See [SECURITY.md](SECURITY.md#outbound-connections).
|
|
152
155
|
|
|
153
156
|
## 2. Get the code
|
|
154
157
|
|
|
@@ -240,8 +243,8 @@ real-time explorer data over RPC was a failed idea.
|
|
|
240
243
|
|
|
241
244
|
`rpcUser` / `rpcPassword` are still accepted, for a node that authenticates with `rpcauth` rather
|
|
242
245
|
than the cookie file (see [Configuration](CONFIGURATION.md#nodes)). The web UI's node-connection
|
|
243
|
-
form takes no password on purpose -- taking one over
|
|
244
|
-
|
|
246
|
+
form takes no password on purpose -- taking one over a web endpoint is not something to add
|
|
247
|
+
quietly -- so credentials go in `config/local.json`.
|
|
245
248
|
|
|
246
249
|
##### `bitcoin.conf` settings worth having
|
|
247
250
|
|
|
@@ -284,17 +287,20 @@ npm run check # every configured node: RPC, credentials, txindex, getblock
|
|
|
284
287
|
npm start
|
|
285
288
|
```
|
|
286
289
|
|
|
287
|
-
Watch the start-up lines. You should see
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
Watch the start-up lines. You should see `listening on https://127.0.0.1:21000` (the first start
|
|
291
|
+
makes the monitor its own self-signed certificate under `data/tls/`; the browser warns once per
|
|
292
|
+
address and remembers it), a line per node, and — because accounts are on by default — `created
|
|
293
|
+
the first admin account (admin)` with a generated password **shown once** (set
|
|
294
|
+
`BLOCKYARD_ADMIN_PASSWORD` before the first start to choose it; `blockyard user` changes it
|
|
295
|
+
later). If the node entry names an `addressIndex` directory with no index in it, `address index:
|
|
296
|
+
building … with N workers -- the Overview shows the progress` follows, and the build runs on in
|
|
297
|
+
the background (see [Building the address index](#building-the-address-index)). Then open
|
|
298
|
+
<https://127.0.0.1:21000> and sign in.
|
|
293
299
|
|
|
294
|
-
Check it from the shell:
|
|
300
|
+
Check it from the shell (`-k`, because the certificate is self-signed):
|
|
295
301
|
|
|
296
302
|
```bash
|
|
297
|
-
curl -
|
|
303
|
+
curl -sk https://127.0.0.1:21000/api/health
|
|
298
304
|
```
|
|
299
305
|
|
|
300
306
|
If a node shows as offline, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md#a-node-shows-offline).
|
|
@@ -330,7 +336,8 @@ If a node shows as offline, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md#a-node-s
|
|
|
330
336
|
```
|
|
331
337
|
|
|
332
338
|
Set `User=` / `Group=` to the account from step 1, `WorkingDirectory=` to where the code
|
|
333
|
-
lives,
|
|
339
|
+
lives, the two `ReadWritePaths=` lines to that directory's `data` and `config` (and add one for
|
|
340
|
+
your address index directory; the unit makes everything else read-only to the service), and replace the `Environment=` lines that name paths with your own (or delete
|
|
334
341
|
them and keep everything in `config/local.json`). Point `ExecStart` at an absolute Node
|
|
335
342
|
22 binary — `/usr/bin/env node` can resolve to an older system Node under systemd:
|
|
336
343
|
|
|
@@ -356,10 +363,10 @@ Where the monitor listens is a security decision. Set `server.host` in `config/l
|
|
|
356
363
|
|
|
357
364
|
| bind | who can connect | typical use |
|
|
358
365
|
|---|---|---|
|
|
359
|
-
| `"127.0.0.1"` | this machine only | reach it with an SSH tunnel: `ssh -L 21000:127.0.0.1:21000 you@host` |
|
|
366
|
+
| `"127.0.0.1"` (the default) | this machine only | reach it with an SSH tunnel: `ssh -L 21000:127.0.0.1:21000 you@host` |
|
|
360
367
|
| `"192.0.2.10"` (a LAN address) | anything that can route to that address | a home or office LAN |
|
|
361
368
|
| `["192.0.2.10", "198.51.100.7"]` | exactly those addresses | LAN plus a VPN such as Tailscale or WireGuard |
|
|
362
|
-
| `"0.0.0.0"`
|
|
369
|
+
| `"0.0.0.0"` | every interface | behind a firewall you control |
|
|
363
370
|
|
|
364
371
|
Notes:
|
|
365
372
|
|
|
@@ -377,15 +384,17 @@ Notes:
|
|
|
377
384
|
- `server.allowCidrs` (or `BLOCKYARD_ALLOW_CIDRS=192.0.2.0/24,2001:db8::/32`) makes the
|
|
378
385
|
monitor itself refuse clients outside those networks, as a second line of defence.
|
|
379
386
|
|
|
380
|
-
## 8. Accounts (
|
|
387
|
+
## 8. Accounts (on by default)
|
|
381
388
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
389
|
+
Sign-in is required out of the box. The first start with an empty data directory creates an
|
|
390
|
+
`admin` account and prints its password **once** in the log (under systemd: `journalctl -u
|
|
391
|
+
blockyard`). Set your own instead with `BLOCKYARD_ADMIN_PASSWORD` for that first start.
|
|
385
392
|
|
|
386
|
-
To
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
To open the monitor to readers with no account, set `"auth": { "enabled": false }` or
|
|
394
|
+
`BLOCKYARD_AUTH=0` and restart: anyone who can reach the port then reads it as a `viewer` —
|
|
395
|
+
charts, the explorer, the event stream, the read-only RPC console — while user
|
|
396
|
+
administration, the audit trail and every node write stay closed. The boot log names the
|
|
397
|
+
addresses that leaves readable.
|
|
389
398
|
|
|
390
399
|
Manage accounts from the Admin page, or from the shell — for example, to reset the admin
|
|
391
400
|
password:
|
|
@@ -398,17 +407,35 @@ Roles: `viewer` reads; `operator` may also run node actions that you have enable
|
|
|
398
407
|
also manages users and reads the audit trail. Node actions are off unless you enable them
|
|
399
408
|
explicitly — see [SECURITY.md](SECURITY.md#node-writes).
|
|
400
409
|
|
|
401
|
-
## 9. HTTPS (
|
|
410
|
+
## 9. HTTPS (on by default)
|
|
402
411
|
|
|
403
|
-
|
|
412
|
+
Every listener serves HTTPS out of the box. With no certificate of your own named, the first
|
|
413
|
+
start makes one: a self-signed certificate and key under `<data>/tls/` (`data/tls/cert.pem` and
|
|
414
|
+
`key.pem`, the key readable by the service account only), naming the addresses the monitor is
|
|
415
|
+
reached on — the bound hosts, this machine's addresses and hostname, `localhost`. It is remade
|
|
416
|
+
by itself when it nears expiry (825 days) or stops naming a bound address. Browsers warn once
|
|
417
|
+
per address about a self-signed certificate and then remember it; the start-up log prints its
|
|
418
|
+
fingerprint so you can compare.
|
|
419
|
+
|
|
420
|
+
To add a name or address (say, a DNS name you gave the machine) or start over:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
blockyard tls --san monitor.lan.example # or: node scripts/tls.js --san ...
|
|
424
|
+
blockyard tls --force # a fresh key and certificate
|
|
425
|
+
blockyard tls --print > blockyard.crt # the certificate, for another machine's trust store
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
To use a certificate of your own instead, name it and every listener serves that:
|
|
404
429
|
|
|
405
430
|
```bash
|
|
406
431
|
BLOCKYARD_TLS_CERT=/etc/blockyard/cert.pem BLOCKYARD_TLS_KEY=/etc/blockyard/key.pem npm start
|
|
407
432
|
```
|
|
408
433
|
|
|
409
|
-
or in `config/local.json`: `"server": { "tls": { "cert": "...", "key": "..." } }`.
|
|
434
|
+
or in `config/local.json`: `"server": { "tls": { "cert": "...", "key": "..." } }`. To serve plain
|
|
435
|
+
HTTP — behind a reverse proxy that terminates TLS (section 10) — set `BLOCKYARD_TLS=0` or
|
|
436
|
+
`"server": { "tls": { "enabled": false } }`.
|
|
410
437
|
|
|
411
|
-
|
|
438
|
+
If you would rather make the certificate with openssl yourself:
|
|
412
439
|
|
|
413
440
|
```bash
|
|
414
441
|
sudo mkdir -p /etc/blockyard
|
|
@@ -424,8 +451,8 @@ rather than silently serving plain HTTP.
|
|
|
424
451
|
|
|
425
452
|
## 10. Behind a reverse proxy (optional)
|
|
426
453
|
|
|
427
|
-
If you already run nginx, Caddy or similar, bind the monitor to `127.0.0.1
|
|
428
|
-
terminate TLS. The live stream (`/api/stream`) is Server-Sent Events, so the proxy must not
|
|
454
|
+
If you already run nginx, Caddy or similar, bind the monitor to `127.0.0.1`, set
|
|
455
|
+
`BLOCKYARD_TLS=0` so it speaks plain HTTP to the proxy, and let the proxy terminate TLS. The live stream (`/api/stream`) is Server-Sent Events, so the proxy must not
|
|
429
456
|
buffer it. An nginx example:
|
|
430
457
|
|
|
431
458
|
```nginx
|
|
@@ -468,8 +495,41 @@ Your `config/local.json` and `data/` directory are untouched by updates. Read
|
|
|
468
495
|
up new front-end files on the next page load; the header shows a notice when the page you
|
|
469
496
|
have open is older than the server.
|
|
470
497
|
|
|
498
|
+
**Updating from 0.0.9.** The defaults hardened in 0.1.0, and a `config/local.json` written by
|
|
499
|
+
0.0.9's installer does not name them, so the first start after the update behaves like a fresh
|
|
500
|
+
install in three ways:
|
|
501
|
+
|
|
502
|
+
- **HTTPS.** The monitor makes itself a self-signed certificate under `data/tls/` and serves
|
|
503
|
+
HTTPS on the same port; `http://…:21000` stops answering. Open `https://`, accept the
|
|
504
|
+
certificate once. Behind your own reverse proxy, set `BLOCKYARD_TLS=0` (§10).
|
|
505
|
+
- **Sign-in.** Accounts are on. The first start creates the `admin` account and prints its
|
|
506
|
+
password **once** in the log (`journalctl -u blockyard` under systemd); set
|
|
507
|
+
`BLOCKYARD_ADMIN_PASSWORD` before that start to choose it. To keep the monitor open as before,
|
|
508
|
+
put `"auth": { "enabled": false }` in `config/local.json` or start with `BLOCKYARD_AUTH=0`.
|
|
509
|
+
- **This machine only.** With no `server.host` in the config the bind is `127.0.0.1`. A
|
|
510
|
+
0.0.9 config written by the installer names the host it chose, so a LAN bind stays; if yours
|
|
511
|
+
does not, add `BLOCKYARD_BIND` or `server.hosts` (§7).
|
|
512
|
+
- **Market polling is off** until someone ticks **Display settings → Markets & Price → Enable
|
|
513
|
+
market polling** — once, for every screen.
|
|
514
|
+
|
|
471
515
|
## 12. Uninstalling
|
|
472
516
|
|
|
517
|
+
BlockYard touches nothing outside its own directories: it reads `bitcoin.conf` and never writes
|
|
518
|
+
it, and it leaves the node as it found it. Removing it is deleting those directories.
|
|
519
|
+
|
|
520
|
+
**Installed from npm** (`npm install -g blockyard`): stop it, then
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
npm uninstall -g blockyard
|
|
524
|
+
rm -rf ~/.blockyard # local.json, data/ (history, accounts, the audit trail) and data/index
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**A checkout** run with `npm start`: stop it and delete the checkout; `config/` and `data/`,
|
|
528
|
+
the index included, live inside it. If you pointed the index elsewhere at setup time
|
|
529
|
+
(`addressIndex` in `config/local.json`), delete that directory too.
|
|
530
|
+
|
|
531
|
+
**The systemd service** of section 6:
|
|
532
|
+
|
|
473
533
|
```bash
|
|
474
534
|
sudo systemctl disable --now blockyard
|
|
475
535
|
sudo rm /etc/systemd/system/blockyard.service && sudo systemctl daemon-reload
|
|
@@ -484,7 +544,7 @@ sudo userdel blockyard
|
|
|
484
544
|
- [ ] `npm run check` passes: Core 25.0+, `txindex` synced, the block files readable, no pruning
|
|
485
545
|
- [ ] the index directory has ~125 GB free and is writable by the service account
|
|
486
546
|
- [ ] the start-up log shows the addresses you intended, and no node offline
|
|
487
|
-
- [ ] you have decided who can reach the port (bind, firewall, `allowCidrs`)
|
|
488
|
-
- [ ] accounts on if the port is reachable by people who should not see your node
|
|
489
|
-
- [ ] HTTPS on, or a proxy / tunnel in front, if
|
|
490
|
-
- [ ] `BLOCKYARD_MARKETS=0`
|
|
547
|
+
- [ ] you have decided who can reach the port: it binds `127.0.0.1` until you say otherwise (bind, firewall, `allowCidrs`)
|
|
548
|
+
- [ ] accounts stay on (the default) if the port is reachable by people who should not see your node; you have the admin password from the first start
|
|
549
|
+
- [ ] HTTPS is on by default with the monitor's own certificate; a certificate of your own, or a proxy / tunnel in front, if you want no browser warning
|
|
550
|
+
- [ ] **Enable market polling** ticked in Display settings if you want the Markets and Kiosk tabs (off by default); `BLOCKYARD_MARKETS=0` on machines that must make no outbound connections at all
|
package/docs/MEASUREMENTS.md
CHANGED
|
@@ -1252,3 +1252,150 @@ showing STALLED. Block intervals are close to exponential with a 10-minute mean,
|
|
|
1252
1252
|
minutes or more has probability e^-4, about 1.8% -- once in fifty blocks, a few times a day. Stalled
|
|
1253
1253
|
now means a connected peer reports a higher tip (`getpeerinfo` `synced_headers`); peers agreeing on
|
|
1254
1254
|
the tip is a long gap and synced; no peer height at all waits two hours.
|
|
1255
|
+
|
|
1256
|
+
## 32. The DOOM Diversion's emulated PC (2026-09-15)
|
|
1257
|
+
|
|
1258
|
+
Not the node: the i386 and the PC in `public/js/x86.js` and `dospc.js` running the shareware
|
|
1259
|
+
`DOOM.EXE` v1.9 (the Diversion). Taken on this box -- AMD Ryzen 9 9950X3D, Node v22.23.2, Chromium
|
|
1260
|
+
152 (snap, headless). Reproduce the headless figures with `node scripts/dos-bench.js doom`.
|
|
1261
|
+
|
|
1262
|
+
**Speed of the interpreter**, 400 M instructions of DOOM's title and demos on a clock of 30 M
|
|
1263
|
+
instructions to the virtual second:
|
|
1264
|
+
|
|
1265
|
+
| build | instructions a second |
|
|
1266
|
+
|---|---|
|
|
1267
|
+
| first cut: unsigned values (`>>> 0`), flag operands in closure variables, a try/catch per instruction | 66-68 M |
|
|
1268
|
+
| every value an int32, lazy flags in an `Int32Array`, one try/catch around the loop, 32-bit fast paths | 95 M |
|
|
1269
|
+
| the same with the Sound Blaster and OPL3 attached, synthesising at 44.1 kHz per call of `tick` | 73 M |
|
|
1270
|
+
| the OPL's per-register work hoisted out of the sample loop | 82 M |
|
|
1271
|
+
| `tick` working in batches of at least 128 frames (it had been called every 2,000 instructions: a sample or two each) | **92 M** |
|
|
1272
|
+
|
|
1273
|
+
The try/catch alone was 14% of the profile, and the collector 1.5% from doubles boxed in closure
|
|
1274
|
+
variables; the rewrite was checked instruction by instruction against the first cut for 40 M
|
|
1275
|
+
instructions (identical except the start-up's environment read, which the first cut got wrong).
|
|
1276
|
+
|
|
1277
|
+
**In the browser**, the machine in a worker, sound on: **94-110 M instructions a second, 34-37
|
|
1278
|
+
frames a second** -- DOOM's own cap is 35. The game needs about a million instructions a frame of
|
|
1279
|
+
real work; the rest of each tic it spends in its own busy wait for the timer. The page's CSP forbids
|
|
1280
|
+
eval, so a JIT was never an option; a slower machine than this one has roughly a factor of two in
|
|
1281
|
+
hand before DOOM drops below 35.
|
|
1282
|
+
|
|
1283
|
+
**Start-up**: graphics mode after 8.0 M instructions without a sound card, 17.0 M with one (the
|
|
1284
|
+
DMX driver probes the DSP and the OPL); 20 pages flipped by 31 M.
|
|
1285
|
+
|
|
1286
|
+
**Correctness of the CPU, measured against the host's own**: a differential fuzzer ran random
|
|
1287
|
+
instructions natively (a C harness in 64-bit mode, 32-bit operands, register forms) and in `x86.js`,
|
|
1288
|
+
comparing all registers and every flag the instruction defines -- ALU rows, immediates, shifts and
|
|
1289
|
+
rotates with counts past the width, MUL/IMUL/DIV/IDIV including divide faults, BT/BTS/BTR/BTC,
|
|
1290
|
+
BSF/BSR, SHLD/SHRD, MOVZX/MOVSX, SETcc, BSWAP, XADD, CMPXCHG, SAHF/LAHF. Two seeds, 78,565
|
|
1291
|
+
instructions, 2,338 divide faults agreed, **zero mismatches**. The harness needs a C compiler, so it
|
|
1292
|
+
is not in `npm test`; `test/x86.test.js` keeps a case from each class it covered.
|
|
1293
|
+
|
|
1294
|
+
**The music is in tune**: over 10 s of the title music, 178 notes keyed on, 96 of them within
|
|
1295
|
+
2.5 cents of equal temperament; the rest spread to +-50 cents, which is DMX's pitch bends.
|
|
1296
|
+
|
|
1297
|
+
## 33. Quake on the same PC (2026-09-15)
|
|
1298
|
+
|
|
1299
|
+
`QUAKE.EXE` v1.06 (DJGPP, go32 stub) on the emulated PC, this box, Node v22.23.2. Reproduce with
|
|
1300
|
+
`node scripts/dos-bench.js quake`.
|
|
1301
|
+
|
|
1302
|
+
**Speed**: 77 M instructions a second headless with the Sound Blaster attached, 72-79 in a Chromium
|
|
1303
|
+
worker -- lower than DOOM's 90-105 because Quake's code is FPU-heavy (the x87 was 15% of the profile)
|
|
1304
|
+
and every memory operand adds a segment base (DJGPP's DS is at its memory block; `ea` was 13%).
|
|
1305
|
+
Moving the FPU stack to a Float64Array with typed-array operand conversion and one base addition
|
|
1306
|
+
when DS and SS share it took 74 to 76: the interpreter's dispatch is the rest.
|
|
1307
|
+
|
|
1308
|
+
**Frame rate**: `+timedemo demo1` on a clock of 74 M instructions to the virtual second -- the
|
|
1309
|
+
emulator's own speed -- reported **969 frames in 33.5 seconds, 28.9 fps**, about 2.6 M instructions a
|
|
1310
|
+
frame. In the browser, a new game on the start map drew 26 frames a second. Period hardware for
|
|
1311
|
+
comparison: a Pentium 90 ran the same demo at 320x200 at roughly that rate.
|
|
1312
|
+
|
|
1313
|
+
**Start-up**: graphics mode after 244 M instructions on a 30 M clock (Quake pages its 27 MB heap in
|
|
1314
|
+
and times its hardware), twenty screens drawn by 285 M; 2.8 s of wall time headless.
|
|
1315
|
+
|
|
1316
|
+
## 34. The decoded-instruction cache, and Quake's view size (2026-09-15)
|
|
1317
|
+
|
|
1318
|
+
This box, Node v22.23.2, `node scripts/dos-bench.js doom 400` / `quake 1500` (M instructions a
|
|
1319
|
+
second, headless, Sound Blaster attached):
|
|
1320
|
+
|
|
1321
|
+
| build | DOOM | Quake |
|
|
1322
|
+
|---|---|---|
|
|
1323
|
+
| before (the interpreter, §32-33) | 90 | 77 |
|
|
1324
|
+
| a closure per decoded instruction | 68 | 69 |
|
|
1325
|
+
| int32 decodings in a typed array per page, one switch; a write drops the whole page | 83 | 90 |
|
|
1326
|
+
| ...a write clears only the instructions it overlaps (DOOM patches its span drawer each call) | 100 | 90 |
|
|
1327
|
+
| ...the FPU decoded too, and the loop keeps the current page between instructions | 107 | 104 |
|
|
1328
|
+
| ...no page check after a cached handler | **111** | **108** |
|
|
1329
|
+
|
|
1330
|
+
The page-drop row is the one to remember: 217,660 whole-page drops in 300 M instructions of DOOM,
|
|
1331
|
+
every one from `mov [eax],ebx` into the constants of its own span routine at 0x12bdaf.
|
|
1332
|
+
|
|
1333
|
+
**Quake's view size**, `+viewsize N +timedemo demo1` (969 frames), on a clock set to the emulator's
|
|
1334
|
+
speed:
|
|
1335
|
+
|
|
1336
|
+
| viewsize | before the cache (76 M clock) | after (105 M clock) |
|
|
1337
|
+
|---|---|---|
|
|
1338
|
+
| 100 | 29.7 fps | 40.9 fps |
|
|
1339
|
+
| 80 (the new default) | 32.5 | 44.7 |
|
|
1340
|
+
| 60 | 40.5 | -- |
|
|
1341
|
+
|
|
1342
|
+
In a Chromium worker: Quake 99-106 MIPS and 40-42 frames a second in a new game at `viewsize 80`
|
|
1343
|
+
(26 before), DOOM 133 MIPS at its 35 fps cap.
|
|
1344
|
+
|
|
1345
|
+
**Checked, not assumed**: both games lock-stepped against the uncached interpreter (DOOM 400 M, Quake
|
|
1346
|
+
1.5 G instructions; registers and flags compared every 10,000; memory identical at the end), and the
|
|
1347
|
+
native fuzzer re-run through `run(1)` over 118k instructions with no mismatch.
|
|
1348
|
+
|
|
1349
|
+
## 35. A second pass on the CPU: what paid and what did not (2026-09-15)
|
|
1350
|
+
|
|
1351
|
+
Asked for all five of: split the ALU routine, specialise the hot x87 forms, cheaper dispatch, dead
|
|
1352
|
+
flags, and a block copy of the frame into video memory. **Method, after the first readings misled:**
|
|
1353
|
+
each build against a copy of the previous one (`oracle4`), pinned to one core with `taskset`, best of
|
|
1354
|
+
three; unpinned runs on this shared box moved +-5% between identical runs. Quake throughput as M
|
|
1355
|
+
instructions a second over its timedemo, and as **frames a second of wall time** over 600 timedemo
|
|
1356
|
+
frames -- the second catches work a MIPS figure cannot, such as a `rep movsd` that is one instruction
|
|
1357
|
+
however many bytes it moves. DOOM on `-timedemo demo1` only: its normal MIPS depends on how much time
|
|
1358
|
+
lands in its cheap wait loop, which a change to the machine's slice size alone moved by 5%.
|
|
1359
|
+
|
|
1360
|
+
| build | Quake MIPS | Quake fps (wall) | DOOM timedemo MIPS |
|
|
1361
|
+
|---|---|---|---|
|
|
1362
|
+
| before (the cache, §34) | 101 | 37.7 | 103 |
|
|
1363
|
+
| ALU split per operation + hot x87 forms decoded to their own handlers | 133 | -- | -- |
|
|
1364
|
+
| + fused cmp/test+Jcc dispatch and "no flags" forms by flag liveness | 131 | -- | 110 |
|
|
1365
|
+
| the same without the look-ahead (fusion and no-flags off) | 133 | -- | 114.5 |
|
|
1366
|
+
| + the address formed inline in the loop instead of a call per handler | 141.5 | -- | 112 |
|
|
1367
|
+
| + aligned reads/writes inline in the hottest moves | 141 | 52.5 | 115 |
|
|
1368
|
+
| the same without the VGA block copy | 141 | 51 (noise) | -- |
|
|
1369
|
+
|
|
1370
|
+
**Kept**: the ALU split, the x87 handlers, the inline address and moves -- Quake's frames a second of
|
|
1371
|
+
wall time 37.7 -> 52.5 (+40%). **Removed**: the fused branches and the no-flags forms (nothing gained;
|
|
1372
|
+
the look-ahead re-ran on every re-decode of DOOM's self-patching drawer), and the block copy (within
|
|
1373
|
+
noise: the frame copy is 64,000 bytes against 140 M instructions a second).
|
|
1374
|
+
|
|
1375
|
+
The reason inlining mattered: `run()` is one function with 170-odd cases, V8's cumulative inlining
|
|
1376
|
+
budget runs out long before the helpers it calls, and every uninlined `eaOf` was a real call.
|
|
1377
|
+
|
|
1378
|
+
**In a Chromium worker** (new game, `viewsize 80`): Quake 115-118 MIPS and 39-49 frames a second on
|
|
1379
|
+
screen, once the worker looked for a finished frame every 50,000 instructions instead of once per
|
|
1380
|
+
10 ms slice (two frame copies inside one slice had been showing as one). DOOM 139 MIPS.
|
|
1381
|
+
|
|
1382
|
+
Checked: DOOM 400 M and Quake 1.5 G instructions lock-stepped identical to `oracle4` (memory equal),
|
|
1383
|
+
and the native fuzzer through `run(1)`, 78k instructions, no mismatch.
|
|
1384
|
+
|
|
1385
|
+
## 36. Wolfenstein 3D in real mode (2026-09-15)
|
|
1386
|
+
|
|
1387
|
+
`WOLF3D.EXE` v1.4 (LZEXE-packed, Borland C, real mode) on the emulated PC, this box, Node v22.23.2,
|
|
1388
|
+
pinned to one core. It unpacks itself and reaches the sign-on screen in 1.0 s of wall time (60 M
|
|
1389
|
+
instructions on a 20 M clock).
|
|
1390
|
+
|
|
1391
|
+
**Speed**: in a game, turning on the first map, **65 M instructions a second** headless (twice the same
|
|
1392
|
+
reading) and 68-69 in a Chromium worker. Real-mode code runs through the uncached `step()`, so this is
|
|
1393
|
+
the plain interpreter's speed; the game needs about **285 k instructions a frame**, so its 70 frames a
|
|
1394
|
+
second (the VGA's refresh, which it waits for) take 20 M a second -- a third of what is there, and the
|
|
1395
|
+
browser shows 70 frames a second.
|
|
1396
|
+
|
|
1397
|
+
**DOOM and Quake unchanged**: best of three against a copy of the previous build (`oracle5`), DOOM
|
|
1398
|
+
timedemo 93.3 vs 93.0 M a second, Quake 126.3 vs 127.7. Quake lock-stepped identical for 600 M
|
|
1399
|
+
instructions, memory equal. DOOM lock-steps identical to 175 M and then differs, by design: at 173 M it
|
|
1400
|
+
copies between VGA pages in write mode 1, which the previous build wrote as plain data, and reads the
|
|
1401
|
+
planes back.
|