GenX‑DOSWIKI SearchPromptArticle↩ Site

JSVecX — GCE Vectrex

The Vectrex is the odd one out on this site twice over. It is the only console here with its own screen built in — no television, so no 4:3 bezel, just a photo of the whole machine standing on the wallpaper — and it is the only platform we have shipped, removed, and then shipped again.

Why we removed it

We dropped it on 04/07/2026, and the reason was one game. Spike talks: it is the Vectrex title everyone remembers for having a voice. Under jsvecx it ran perfectly and said nothing at all. A console whose most famous game is silent is a console shipped broken, so out it went and the Sega Master System took the slot.

The note we left behind said the fault was AY-3-8912 sound emulation, and that the same limitation existed in the libretro vecx core, so it was the emulation lineage rather than our bundle. That note was wrong on both counts.

Why it came back

Then vectrexia.com turned up, where Spike speaks. Which meant it was possible — and, once we looked, that site is running jsvecx too. It had simply added the missing piece itself.

The missing piece is not the sound chip. The Vectrex has no speech hardware whatsoever, and neither does the Spike cartridge. What it has is a four-level DAC on VIA port A, and a multiplexor that can route that DAC to the sound line. Speech is played by flicking between those levels very fast: the waveform lives in the timing of the flicks, not in sample depth. Across the whole of Spike, port A only ever holds four values while the gate is open — 0, 48, 160, 208, which as signed 16-bit are 0, +12288, −24576, −12288.

Upstream jsvecx does not emulate that path at all. So we added it, in our fork, taken verbatim from libretro-vecx rather than tuned by ear.

Two details are worth knowing, because neither is obvious and both will bite anyone reimplementing this:

Proving it rather than trusting our ears

The whole reason the platform was removed was a judgement made by listening, and that judgement was wrong. So the DAC went in against a measurement instead.

A headless harness runs the emulator with no display and counts how often the DAC is gated open. The reference implementation, libretro-vecx, gives 20,702 for Spike and 0 for Mine Storm. Our JavaScript gives 20,530 and 0 — 0.8% apart. That number says something useful beyond "it works": jsvecx's 6809 and VIA emulation were always faithful, and only the DAC was ever missing.

One trap in running it: with no buttons pressed the game sits on its title screen and the DAC never fires, which looks exactly like a broken DAC. Drive the input.

The same harness then corrected the game documentation. Run over all ten cartridges, it shows that only Spike produces multi-level DAC output. Berzerk uses the DAC not at all — so the arcade machine's "Chicken! Fight like a robot!" taunt, which our gamedoc claimed the port retained, never existed on this machine. Star Castle, Cosmic Chasm and Rip-Off gate the DAC open at a single unchanging level, which is the analogue path rather than sound content.

Three sound chips, one of which works

Upstream ships three implementations that each define e8910() — processed/e8910.js, js/e8910Full.js, and an AudioWorklet built from a minified string inside js/audioWorkletWrapper.js. Whichever script loads last wins. (Our bundle carries two: the worklet file was pruned, for the reason below.)

Tested in Chrome with an analyser tapped between the gain node and the destination, the AudioWorklet path produces RMS 0.001 and peak 0.002 — silence — while the ScriptProcessorNode in e8910Full.js gives RMS 0.17–0.37 with peaks near 0.99. Pristine upstream behaves the same, so this is not something our fork caused. It does mean upstream's own shipped build has no working audio, because that build is precisely the one that excludes e8910Full.js.

So the bundle loads js/e8910Full.js last, deliberately. The fork carries the same DAC in the worklet as well, in case that path is ever fixed, but nothing relies on it.

The audio rate is effectively fixed at 22050 Hz: SOUND_FREQ is used in exactly one place, to construct the AudioContext, while the PSG's period maths uses a hard-coded STEP3 = 1. Raising the rate transposes all the music down an octave.

What we changed besides the DAC

The emulator paused itself whenever the window lost focus — it called stop() and forced its menu open on blur, so clicking any other window froze the machine. Wrong for an embedded player, and it makes measurement treacherous, since any unfocused reading looks like a dead emulator. Removed. Genuine backgrounding via visibilitychange is left alone.

ROMs are stored flat, as games/<key>.bin with overlays/<key>.png beside them, driven by a single ?game=<key> like every other bundle here. Upstream nested them in collection folders and derived the overlay name by splitting the filename on its _<year> suffix. It also accepted remote URLs fetched through a proxy — a runtime network call this site does not make. That path is gone.

minestrm is deliberately not a file: Mine Storm lives in the boot ROM, so it sets its overlay and leaves the machine bare.

What we pruned

The bundle-add prune paid out again — five files, 43 KB. css/seamless.css was never linked; js/dnd.js was referenced by nothing; js/audioWorkletWrapper.js is one of the three e8910() definitions above, and the silent one.

The interesting one was the ROM picker (js/romList.js + js/table.js, 22 KB). It was hidden behind CSS and unreachable, but still ran on every load and populated 486 entries for ROMs the bundle does not ship. Upstream used that table's existence as doinit()'s "already initialised" flag, so removing it needed a boolean in its place. The tell was console output — "486 roms in list" on every load — not a file scan. Worth remembering: an orphan that still executes will not show up as an unreferenced file.

An earlier prune left a loose end, found by the September orphan audit. The touch overlay's two files — css/touch.css and overlay_touch.html — went with the rest of the upstream chrome, but the line that asks for them stayed: on the first touch anywhere on the page, js/input.js fetched both, and both were gone. A desktop never noticed. The listener is no longer registered, which takes the whole touch path out in one line, since nothing in it runs until that listener fires; the disabled line is left in place, commented, with the reason beside it. Restoring touch controls means restoring both files and that line.

Testing that fix with a cartridge rather than the built-in Mine Storm turned up a second one, on a path that runs every time: upstream keeps an optional <key>.txt beside each ROM and shows it before the game starts, and loadNotice() asked for it on every load. We ship the ten .bin files and no notices, so every game boot 404'd. A missing notice was always handled — the 404 set waitForNotice false and the game started — so the function now returns that state directly. Put a <key>.txt in games/ and delete three lines to turn notices back on.

The bundle has one other dangling fetch, deliberately left: toggleRTM() loads js/rtm.js, also pruned, but only when a URL carries ?rtm=true — an upstream debug switch nothing here sets and no player will type.

The overlays

Vectrex games shipped with a printed plastic screen cover, because the display is monochrome — the colour you remember is physical. We ship the eleven overlays and lay each one in front of the vectors inside the bezel aperture, which is why Spike has a blue sky and a red logo on a machine that could only ever draw white lines.

The bezel

The screen hole is a clean rectangle — width holds at 646–647px across the full height, height at 804–811px across the full width — so it needs none of the inscribed-rectangle fitting the TRS-80 and Electron did.

One measuring trap: the usual transparent-pixel bounding box returns 100% × 100% here and is useless, because the console floats on a transparent surround rather than filling the frame. Flood-fill from the hole centre instead. Any bezel that is a photograph of a whole machine will behave the same way.

The canvas and overlay both need !important on their geometry. The engine's own resizer() runs on every resize and writes inline width/height/left/top on both, sizing them to the viewport for its full-screen layout, and inline styles beat ordinary rules.

The controller

The Vectrex is the one machine on this site whose controller was genuinely analogue, and jsvecx drives those axes directly: it polls the Gamepad API around 180 times a second and reads both stick axes plus all four face buttons. So a USB pad here is not a convenience, it is the original input — how far you push the left stick counts, not just which way. Pole Position is the game that shows it. The face buttons map in the Vectrex's own row order: 1, 2, 3, 4 are X, Y, A, B.

Nothing about this is per-game. The analogue stick and its four buttons are the machine's only controller, so every cartridge reads them.

A pad is handed over by the browser only once it has been pressed on a focused page, so the first press may go nowhere -- click the screen and press again. controls.html covers the keyboard equivalents for the whole stick and all four buttons, so nobody is stuck without hardware.

Eleven gamedocs carried "This emulator is keyboard-only — USB gamepads aren't supported" for as long as the bundle existed, inherited and never checked. They were wrong from the start.

What you get

Ten cartridge games, plus Mine Storm — which lives in the boot ROM rather than on a cartridge of its own.

The Vectrex sits in an unusual position here. It is often said to be public domain, and that is not quite right — Jay Smith and Smith Engineering still hold the rights. What they granted, back in the mid-1990s, is standing permission to reproduce and distribute the software they published, provided nobody profits by it. That fits this site exactly, which is non-commercial by licence as well as by intent, and it is a large part of why the homebrew scene around a machine discontinued in 1984 is still producing new games.

No save states: jsvecx exposes none, so the bundle has no save/load controls.