A desktop front end for the Virtual Jaguar core: SDL3 for window, audio and
input, Dear ImGui for the UI, OpenGL 3.3 core for presentation, on macOS, Linux
and Windows. The core stays C89 and is linked statically; the app drives it
only through the standard retro_* API, the same one RetroArch uses, so every
option and save state behaves exactly as it does there.
Everything lives under app/. The core's own make build is untouched.
Installable packages (macOS zip, Windows installer and portable zip, Linux AppImage and
.deb) are built by CI and attached to the releases: see packaging/README.md.
To package and sign your own build locally (a Developer ID or Apple Development certificate
found in your keychain on macOS, a .pfx or store certificate on Windows, optionally configured in a
gitignored .env), run bash app/packaging/local-package.sh --sign [--notarize] [--build-dir DIR]: see
"Signing locally" in packaging/README.md.
You need CMake 3.20+, a C99/C++17 toolchain and, on Linux, the SDL3 desktop dev packages (see below). SDL3 (3.4.16), Dear ImGui (1.92.8) and rcheevos (12.5.0) are downloaded at configure time, pinned by SHA256. Nothing is vendored in the repo.
cmake -S app -B app/build -DCMAKE_BUILD_TYPE=Release
cmake --build app/build -j
ctest --test-dir app/build -C Release --output-on-failure # headless smoke testsThe binary is app/build/virtualjaguar (app/build/Release/virtualjaguar.exe
with the Visual Studio generator).
FreeType 2.14.1 is the default font rasteriser: it is fetched (pinned, SHA-256 checked), built static with every
optional library off, so it needs no system libraries. -DVJ_APP_FREETYPE=OFF uses Dear ImGui's built-in
stb_truetype instead (no hinting or anti-aliasing choices); offline builds use -DFETCHCONTENT_SOURCE_DIR_FREETYPE=<dir>.
Use an already installed SDL3 instead of fetching one: -DVJ_APP_SYSTEM_SDL3=ON
(Homebrew sdl3, distro libsdl3-dev). For offline builds point
-DFETCHCONTENT_SOURCE_DIR_SDL3=<dir> and -DFETCHCONTENT_SOURCE_DIR_IMGUI=<dir>
at unpacked sources.
Xcode command line tools and CMake (brew install cmake). No extra libraries.
The host make build of the core needs DEVELOPER_DIR=/Library/Developer/CommandLineTools
to avoid App Management prompts; the CMake/C++ build of the app does not, and
setting it there can hide the C++ standard library headers.
sudo apt-get install cmake pkg-config libasound2-dev libpulse-dev libx11-dev \
libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev \
libxtst-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libudev-dev libwayland-dev wayland-protocols \
libdecor-0-dev libcurl4-openssl-devlibcurl4-openssl-dev is only for the game library's box art and database downloads
(see Game library); without it the app still builds and runs, it just never
downloads anything.
MinGW-w64 (MSYS2 mingw-w64-x86_64-gcc, cmake, ninja) is the supported
toolchain: it is what the core's own Windows release build uses.
cmake -S app -B app/build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build app/buildMSVC (cmake -S app -B app/build then cmake --build app/build --config Release)
is wired up and runs in CI as an experimental, non-gating job: the repo's
msvc-check job has never compiled about 31 of the core's sources with cl.exe
(scripts/check-msvc-sources.sh, issue #679), so full-core MSVC is unproven.
app/cmake/VJCore.cmake parses the source list out of Makefile.common
(the same regexes as scripts/check-package-sources.sh), so adding a .c there
needs no change in app/. The flags mirror what make -n passes: -D__LIBRETRO__,
-DINLINE=inline, the warning set, -fno-strict-overflow, libchdr's own C99
translation unit. The SIMD blitter variant is chosen by the compiler: all three
blitter_simd_<arch>.c are built with -DBLITTER_SIMD_AUTODETECT, exactly as
Package.swift does (CI checks that exactly one blitter_simd_ops exists).
src/core/version.h is generated at configure time when sh is available.
No -std=gnu89 is forced: make passes no -std either, and gnu89 changes
inline linkage. C89 conformance is gated by scripts/c89-lint.sh.
virtualjaguar game.j64 # cartridge (HLE boot, no BIOS needed)
virtualjaguar disc.cue # or .chd / .cdi
virtualjaguar --option virtualjaguar_usefastblitter=enabled game.j64
virtualjaguar --helpOpen a game with File > Open (Ctrl/Cmd+O), by drag and drop onto the
window, or on the command line. File > Open recent keeps the last ten.
Loading another game while one is running does a full unload, retro_deinit,
retro_init, retro_load_game cycle.
| Key | Action |
|---|---|
| Arrows, Z, X, C, Enter, Right Shift | D-pad, B, A, C, Option, Pause |
number row / numpad, - = |
Jaguar keypad 0-9, *, # |
| A S D, Q E | Jaguar Pro Controller X Y Z and left / right shoulder (keypad 9 8 7, 4 6) |
| F2 | keypad overlay |
| F3 | controls overlay: a cheat sheet of your current bindings (also View menu) |
| Esc / F10 | release the captured mouse; F10 or a click on the picture captures it again (see below) |
| F5 / F7 / F6 | save state / load state / next slot (Shift+F6 previous) |
| F8, Ctrl/Cmd+R | pause, reset |
| Tab (hold) | fast-forward |
| F11, Alt+Enter, Esc | fullscreen, leave fullscreen |
| F1 | pin or hide the menu bar in fullscreen |
Gamepads use SDL's standard layout (South/East/West = B/A/C, Back = Pause, Start = Option); the left stick also drives the D-pad. Controller types per Jaguar port (Team Tap, rotary, ...) are under Settings > Input > Devices & players.
Settings > Input edits the bindings: pick a device (the keyboard, every
gamepad, or one connected controller model), pick the scope (everywhere, or
this game only), then Set / Add a binding and press the key, button or
stick direction ("press a button to bind"; Esc cancels), Unbind, or go back
to Default. Per control, the first of these that defines it wins: this game
and this controller, this game, this controller, everywhere, built-in default.
Games are identified by the CRC32 of the cartridge image (the key the core's
title database uses; copier headers stripped); discs fall back to the file name.
Everything is stored in config.ini as input.* keys. Controllers SDL does not
know can be added with a gamecontrollerdb.txt (SDL's format) in the data folder.
The default gamepad layout reaches all twelve keypad keys without the overlay:
| pad | keypad |
|---|---|
| North, L1, R1, L2, R2, L3, R3 | 0, 1, 2, 3, 4, 5, 6 |
| right stick left / up / right / down | 7, 8, 9, * (dominant axis only, never two keys) |
| Guide | # |
| Share / Capture, or F2, or View > Keypad overlay | on-screen keypad (D-pad moves, A/B presses, C closes) |
How it reaches the core: pad buttons ride the RetroPad bitmask; the RetroPad has
no button for keypad 7, 8, 9, *, #, so keypad keys are delivered as the
RETRO_DEVICE_KEYBOARD keys libretro.c polls (player 1: 0-9 - =; player 2:
p q w e r t y u i o [ ]). The app announces a keyboard after every load, which
the core needs before it polls them. This is the core's default input path, so the
core's own remapping options (virtualjaguar_alt_inputs, *_numpad_to_kb, *_retropad_*
and the per-port device options) are forced to their defaults and hidden: this tab
and Devices & players do all of that. Team Tap sockets read only the RetroPad, so
there keypad 0-6 ride X, L, R, L2, R2, L3, R3 and keypad 7-9, *, # are unreachable
(a limit of the core).
Settings > Input > Devices & players (everywhere, or this game only; stored as
input.port.N / input.game.<id>.port.N) picks the device in each Jaguar port from the
core's own list and calls retro_set_controller_port_device:
| device | driven by (defaults, all rebindable under Key / button bindings) |
|---|---|
| Standard Joypad, Pro Controller | the pad and keypad bindings above |
| ST / Amiga mouse (port 2) | the mouse or trackpad (captured); or the right stick / arrows; Z X or South East are the buttons |
| Rotary (either port) | mouse left / right, or two keys (Left / Right), or the right stick; speed in Tuning |
| Analog, driving, paddle | left stick or A D W S; driving: Right trigger / E accelerator, Left trigger / Q brake; D = North or V |
| 6D | both sticks (A D W S, J L I K), triggers (Q E), shoulders (U O), A-F, Option = G, Pause = Rezero |
| Light gun (port 1) | the mouse over the picture, left click fires, right click (or R) fires off-screen to reload; a stick or arrows can aim too |
| Team Tap | one controller (or the keyboard) per socket, chosen under Players |
The Players section lists the active player slots (two, plus three more behind each Team Tap) and which controller plays each: Automatic hands controllers out in plug-in order (the first two play players 1 and 2, as before), or pick one by model and number. The light gun aims with the rectangle the picture is drawn in, so scaling, aspect overrides, integer scaling and HiDPI all land the shot on the pixel under the pointer.
The mouse is captured (SDL relative mode) only while a mouse or rotary is in a port, the game window has focus, the game is running and no menu, popup or Settings window is open. Esc or F10 releases it (F10 may need Fn on a Mac keyboard); click the picture or press F10 to capture again.
The controls overlay (F3, the Touchpad button on a gamepad, or View > Controls overlay) shows the bindings in effect for Player 1's device - the keyboard's or the gamepad's, whichever you last used - and never takes input. A click or tap anywhere, Esc or F3 hides it (the click is not passed to the game). It is not shown by itself: a game that starts with no gamepad on Player 1 gets a short "Press F3 for the controls" hint, once per session. Settings > Input > Devices & players > Show the controls overlay when a game starts (off by default) brings the overlay up at boot instead - for keyboard players every time, for gamepads once per controller model - and hides it again after a few seconds.
Settings > Core options is generated from the core's options v2 definitions,
categories and SET_CORE_OPTIONS_DISPLAY visibility included, so a new core
option shows up with no change to the app. Changes are saved; options that say
"Restart required" apply on Restart game to apply. Every core option category and
every other Settings tab has a Reset group button (it skips options the app locks and
options a Netplay session or --option pins), and Reset all settings... asks first and
leaves library folders, recents, covers, saves, the RetroAchievements login, key and button
bindings and per-game overrides alone unless you tick the matching boxes.
Pacing follows the core's reported field rate (60.05445 Hz NTSC, 50.08013 Hz PAL). Audio is opened at the core's rate and resampled by SDL, with a small playback-ratio correction against the buffered amount so it tracks the video.
Settings > Video > Shader (or View > Shader) runs the picture through a
RetroArch .slangp preset, on the GL 3.3 presenter, unmodified. Presets come
from librashader (MPL-2.0),
which the app loads at run time: the build needs no Rust and links nothing
extra. If the library is missing, the wrong version, or a preset does not
compile, the picture is drawn plain and a one-line message says why; it is never
a failure.
- Library. At configure time CMake fetches librashader's two MIT-licensed C
headers (tag
librashader-v0.12.0, pinned by SHA256;-DVJ_LIBRASHADER_INCLUDE_DIR=<dir>for offline builds,-DVJ_APP_SHADERS=OFFto leave it out). At run time the app looks forlibrashader.dylib/.so/.dllin$VJ_LIBRASHADER(a file or folder; a path that does not exist means "missing"), next to the executable (and../Frameworksin a macOS bundle), in<data folder>/librashader/, and then the system library path (Homebrew's/opt/homebrew/libincluded). On macOS and Windows the build downloads the pinned prebuilt runtime (arm64/x86_64) and copies it next to the executable (-DVJ_APP_FETCH_LIBRASHADER=OFFfor offline builds). Upstream publishes no Linux binary: install the distro/OBS package or build it, and it is picked up from the system path. - Presets.
shaders/beside the executable holds two small first-party presets (vj-sharp-bilinear,vj-scanlines, also the offline test fixtures) and, by default, a subset of libretro/slang-shaders undershaders/slang-shaders/: the CRT (CRT-Royale, CRT-Geom, CRT-EasyMode, ...), scanline and interpolation families plus every directory they reference (~23 MB, ~900 files). CMake downloads the tarball for one pinned commit at configure time (SHA256-verified; nothing is committed to this repo), extracts those directories untouched and copies them beside the executable. Licensing: slang-shaders has no single licence; each shader states its own (GPL, MIT, public domain, ...) in its header and some directories carry LICENSE/README/THANKS files, all of which travel with the copy together with aNOTICE-virtualjaguar.txt. No network, or a hash mismatch, only means no bundled presets (a warning, never a build failure);-DVJ_APP_BUNDLE_SLANG_SHADERS=OFFskips it for distro/offline builds and-DVJ_SLANG_SHADERS_ARCHIVE=<tarball>supplies a local copy. For the rest of slang-shaders, or a newer copy, set Preset folder to a full checkout; the list is searched recursively and filterable, and preset parameters get sliders. - Scaling. Integer scaling and the aspect modes are independent of shaders
(
computePresentRectinpresent_util.cpp): the chain renders into a texture of exactly the rectangle they pick, so a CRT preset at 3x integer scale looks the same as the plain picture at 3x. Both paths share that one function. - Settings keys.
shader.enabled,shader.preset,shader.folder.
vj_shader_tests (CTest shaders_unit, shaders_bundled, shaders_library, shaders_gl) covers
the pure logic and the missing-library fallback anywhere; the other two need the
library (and a display, for the GL one) and report skipped, not passed,
without them. Point VJ_LIBRASHADER at the library, and optionally
VJ_TEST_PRESETS="a.slangp;b.slangp" / VJ_TEST_SLANG_DIR=<checkout>, to run them.
The library is an optional grid of your games. It never replaces the file picker, drag and drop or the command line, which all keep working.
Home screen. With no game loaded the main window is the library: search, filter, size
slider, Add folder..., rescan, and a click on a tile launches it. With no folders yet it shows
a short welcome with an Add folder... button and the open / drag-and-drop hints. Tile size
(0.5x to 2.5x, default 1.0x, library.size) is the slider, and also a trackpad pinch (where SDL
reports one: macOS), Ctrl/Cmd+mouse wheel and Ctrl/Cmd + + / - / 0 (reset) while the pointer is
over the grid or the library has the keyboard focus; never during gameplay or while typing in a
field. Loading a game
replaces it with the game; File > Close game brings it back. Settings > Storage > Show the
game library on the home screen (ui.home_library, also View > Library on home screen) turns it
off for the old plain welcome. The separate Game library window (File > Game library, Ctrl/Cmd+L)
stays for in-game use (pick another game without closing this one); on the home screen Ctrl/Cmd+L
puts the cursor in the search box instead of opening a second copy of the same view.
Sections. Tabs split the library into Cartridges, Jaguar CD and BIOS & system (plus Favorites, Recently played and Save states, below)
(the choice is remembered, library.section). BIOS and system files are not games and are never
in the game grids: files are recognized by CRC32 (the Series K and Model M boot ROMs, the two
Stubulator developer ROMs, the retail and developer CD BIOS and the Memory Track cartridge: the
checksums the core itself checks), by the names the core searches for (jagboot.rom, boot.rom,
jagcd.bin, [BIOS] Atari Jaguar ... .j64, ...), by "bios" or "memory track" in the name of a small
file (512 KiB or less), or by a [BIOS] entry in the dat. The core's own system folder is always
scanned for these too (only BIOS files are kept from it), so the tab shows what the core can read.
A BIOS tile says what it is and whether the core's own search finds it where it sits, and a click opens
its menu: Use as the Cart boot ROM / CD BIOS (the core runs exactly that file, whatever it is called and
wherever it lives, see below), Boot (below) and, for a file the core would find in its system folder under the
right name, Copy to the system folder as ... (the fixed names, for RetroArch sharing the folder; never over a
file already there). BIOS files are found anywhere under a library folder (any depth the walk reaches, any
upper/lower case in the name). The Series K image, the Memory Track and files whose size the core rejects get no
copy offer. The core has
every standard image built in, so none is required. Loose (Track N).bin files are skipped like the
tracks a .cue owns.
Favorites, recently played and save states. Click the star on a tile (or right-click > Add to
Favorites) and the game is a favorite; every game that boots, from the library, a drop, File > Open or the
command line, goes to the front of Recently played (the last 20; right-click > Remove from Recently
Played, or Clear). Where each list shows is up to you, separately, under Favorites & recents... in the
library toolbar and in Settings > Storage (library.favorites.show, library.recents.show: 0 hidden, 1 its
own tab, 2 a strip at the top of the Cartridges and Jaguar CD tabs, 3 both, the default). A tab lists both
media (favorites by title, recents newest first); a strip is one scrolling row at the top of each media tab with
that media's games only, under the same search and filter. The Save states tab lists every game that has
states, newest first, with the screenshot saved with each state, its slot and when it was saved: click one to
start the game from that moment (a state of the game already running just loads into it), Delete asks first
and removes the state, its screenshot and its RetroAchievements sidecar. Right-clicking any game tile gives
Play, Load state (the game's states with screenshots, newest first), Add/Remove Favorite,
Remove from Recently Played, the cover items, Show in Finder / File Explorer / file manager (the
containing folder) and Copy path. Lists remember a game by path, checksum and dat name together, so moving
a folder or an update that identifies a game does not lose it; a game whose file is away (an unplugged drive)
is skipped but not forgotten. A game booted from outside the library folders is not listed. Hardcore
RetroAchievements mode refuses a state load before leaving the running game.
Resume where I left off. Settings > Storage > Save states: Off / Ask / Always (saves.resume, default
Ask). When a game is closed (File > Close game), left for another game, or the app quits, its state is written
as an Auto-save: its own slot beside the numbered ones (<file name>.auto.state + .auto.png, one per game,
only the latest, written atomically). The next time that game boots, Always loads it right after boot, Ask shows
"Resume from ?" with Resume, Start fresh and an "Always resume without asking" box, Off does
nothing (and writes nothing). The Save states tab and the tile's Load state list show it as Auto-save, and
Delete works on it. Rules: never in RetroAchievements hardcore (state loads are refused, so there is no
prompt either) and never during netplay (a state would desynchronise the link); no auto-save for a game that
ran under 5 emulated seconds (still the boot logo; a quick open-and-close must not replace a real auto-save with
a title screen) or that the crash watchdog flagged (resuming would land in the hang); File > Reload game and
headless runs neither keep nor resume one; picking a state in the library skips the offer. Another running copy
(instance 2+) keeps its own auto-saves in states/instance-N/ so two windows on one game cannot overwrite each
other's; the numbered states stay shared.
The lists live in <data>/library/user.tsv, not in config.ini (rules for several copies in
Running several instances). Save states are named <file name>.slotN.state in
<data>/states/, as always, so two games whose files share a name share states.
BIOS & boot. The BIOS & system tab starts with the same panel as Settings > BIOS (also File > Boot BIOS > BIOS & boot settings...; the home screen's "BIOS & boot..." button reaches it with no folder added), so a BIOS can be chosen with no game loaded and whatever the loaded game is (the core hides the CD options while a cartridge runs, and the Core options tab is empty before the first load).
- Which file. Each role, the console boot ROM (128 KiB) and the Jaguar CD BIOS (256 KiB), is Auto or one
chosen file. Auto is the core's default: a file in the system folder under the names it searches, else the image
built into the core (Auto picks = Series K / Model M / Custom for the boot ROM, Retail / Developer for the CD
BIOS). A chosen file is handed to the core by path through a private call (
VJ_ENV_GET_BIOS_PATH,docs/app-frontend-extension.md), so it needs no copying and no special name. The core still checks it (exact size; a CD BIOS also by checksum or header) and, if it refuses, uses its own choice and the panel says so. The choice is saved per role (bios.cart,bios.cdin config.ini; Auto = no key) and applies when the game starts. - When it is used. The boot ROM runs when cartridges boot through the Real boot ROM (instead of HLE), for GPU-only demos, and on Boot. The CD BIOS runs when CD games boot through Real BIOS (instead of HLE), for audio CDs and on Boot. These two switches are the core options BIOS (Cartridges) and CD Boot Mode, shown here whatever is loaded. Panel lines say "Running now: ..." from what the core reported staging (built in, system folder file, or your file, with its CRC32) and warn when a chosen file is idle because the matching switch is HLE.
- Boot. Boot the CD BIOS (no disc) / Boot this ROM (empty slot) (panel, tile menu, File > Boot BIOS)
starts the console with nothing inserted: the CD BIOS comes up on its disc screen with the Memory Track plugged
in; the boot ROM shows the Jaguar logo on an empty slot, as the hardware does. A tile's Boot uses that file
for this boot only. The known Memory Track cartridge dump boots as a cartridge (its own manager program); a
Memory Track image the core does not recognise, a wrong-sized file or another system file is disabled with the
reason in a tooltip. Command line:
--boot-bios cart|cd,--cart-bios FILE,--cd-bios FILE. - Import. Import BIOS file..., or dropping (or File > Open'ing) a boot ROM or CD BIOS on the window, copies
it to
system/bios/(never over a different file), chooses it and rescans. A dropped BIOS is imported, not run as a cartridge; the dialog also takes a dump with no recognisable name, by its size.
File > Game library (Ctrl/Cmd+L) opens the window.
- Add folder... one or more folders. They are walked recursively (8 levels, symlinked
folders inside them are not followed) for the formats the core reads: cartridges
.j64 .jag .rom .abs .cof .prg .binand discs.cue .chd .cdi. A.binthat a.cuereferences is a disc track, not a cartridge, and so is any.binover 8 MiB. Zip is not scanned: the core cannot load it. - Identification by CRC32 against libretro-database (see licences below):
- cartridges against the No-Intro "Atari - Jaguar" dat, using the whole file first and then
the file minus a 512-byte copier header (the same rule as the core's
DetectPrependedHeaderSize, so a headered dump of a known game still matches); - CD images against the Redump "Atari - Jaguar CD" dat. Redump hashes each track, so a
.cueis identified by the CRC32 of the first file it references (Track 1). A.chdis identified the same way: its Track 1 sectors are read back as the Redump.binhas them (the way the core reads them: subcode dropped, audio put back in.binbyte order, a virtual pregap left out). A.chdwith several audio tracks and no session metadata (which the core refuses too) is not hashed. A.cdiis hashed best-effort from its Track 1 descriptor; rips whose layout differs from the descriptors (the core repairs these) will not match. This is verified against synthetic images; real Redump CHDs are still to be checked. - when the checksum does not match (a bin/cue or
.cdithat is not a Redump dump) the disc is matched by name: the file name, then the name of its folder, against the dat's titles with case, punctuation, region and(Track N)/(Disc N)tags ignored. An exact title wins; a file name that is the start of exactly one longer title also matches (file name only, 4+ characters). A name that fits two games (a two-disc set, "Space" for "Space Ace" and "Space Ace Gold") matches neither: it stays unidentified rather than guessing. A name-only match is badged?(titled and given art from the dat entry), an unidentified file??(titled from its file name, no art). Hover a tile for the path, CRC32 and how it was identified (checksum, file name, folder name, or not at all).
- cartridges against the No-Intro "Atari - Jaguar" dat, using the whole file first and then
the file minus a 512-byte copier header (the same rule as the core's
- Box art comes from libretro-thumbnails
(
thumbnails.libretro.com/Atari - Jaguar/Named_Boxarts/<dat title>.png; the libretro rule replaces& * / :" < > ? \ |with_). It is fetched only for tiles you can see, one title at a time, on a background thread, and cached under/thumbnails/forever: art you have seen works offline. When there is no box art the title screen, then a snapshot, is used; when upstream has none the tile shows a coloured placeholder and the miss is remembered for 30 days. Upstream coverage is the limit here: of the 209 titles in the cartridge dat, 70 have art under their exact dat name (64 box art); of the 26 Jaguar CD titles, 12 have a snapshot and none a box art. (Measured against libretro-thumbnails at the pinned dat's date.) For Jaguar CD titles libretro-thumbnails is asked first and, on a miss, the project site's mirror (jaguar.provenance-emu.com/thumbnails/Atari - Jaguar CD/..., box art from TheGamesDB plus title screens and snaps made with this emulator, hosted until libretro-thumbnails has a Jaguar CD repository, issue #141 there); each source has its own cache folder and miss markers (the mirror's expire after 7 days), so mirror art never hides upstream art that appears later. Setlibrary.art_mirrorinconfig.inito another base URL, or to an empty value to turn the mirror off. **Download box art** turns all downloading off. **Your own covers.** libretro-thumbnails has no Jaguar CD repository at all, so no disc gets downloaded art. A tile with no art is a drawn card with the game's title and aCD/Cartbadge. Right-click a tile > **Set cover image...** (PNG) to give it one, or drop a PNG onto the tile while the library is showing (a PNG dropped anywhere else is opened as before, and fails as not a game); **Remove cover image** undoes it. The PNG is copied to/covers/custom/.png(the dat title when the game is identified, else a checksum of the disc or of its path). For many at once, put PNGs named after the game in/covers/, e.g./covers/Myst (USA).png` (dat title first, then the title shown on the tile), then press Rescan. A cover you set beats the folder, which beats a downloaded image, which beats the card. Only PNG is read: SDL3 has no JPEG decoder and the app adds no dependency for one. Covers larger than 8192 px or 32 MB are refused. - Launch is a click on the tile; it posts the same "open this path" event as the file dialog. Close on launch hides the window afterwards.
Files: <data>/library/scan.tsv (the hash cache: a rescan only stats files whose size and
modification time are unchanged), <data>/library/db/ (databases fetched by Update
database), <data>/thumbnails/, <data>/covers/ (your cover art). Settings are the library.* keys in config.ini.
The two dat files are fetched at configure time, pinned to a libretro-database commit and
SHA256 (VJ_DB_COMMIT in CMakeLists.txt), and copied next to the executable in
libretro-db/. Update database downloads the current files into <data>/library/db/; the
newer of the two copies (by the dat's own version) is the one used. Offline build:
-DVJ_APP_LIBRETRO_DB_DIR=<folder holding 'Atari - Jaguar.dat' and 'Atari - Jaguar CD.dat'>;
-DVJ_APP_BUNDLE_DB=OFF skips them (the library then titles games from file names until
Update database).
HTTP is the only network code. SDL3 has no client, so src/http.cpp is a blocking GET with
three compile-time backends: libcurl (macOS SDK, libcurl4-openssl-dev on Linux, MinGW
if installed), WinHTTP on Windows (part of the OS: no dependency), or none
(-DVJ_APP_HTTP=OFF, or libcurl not found), in which case downloads are simply unavailable.
It is only ever called from worker threads, never from the UI or emulation loop.
Licences. The dats are CC BY-SA 4.0 (libretro-database, derived from No-Intro and Redump
data). They are not committed to this repository, only downloaded and shipped unmodified
beside the binary, with attribution here. The box art is the property of its owners; the app
downloads it to the user's own machine on demand, as RetroArch does, and never redistributes
it. PNGs are decoded with SDL3's own loader (SDL_LoadPNG_IO): no image library was added.
Run as many copies of the app as you like: to test netplay on one machine (host in one window,
join 127.0.0.1 in the other), or for local multiplayer on a big screen. Nothing is locked to a
single instance.
- Any platform: File > New window (Ctrl/Cmd+N) starts another copy. On macOS it runs
open -non the app bundle, which is also what you do by hand: a double-click only activates the copy that is already running, so from a shell useopen -n "/Applications/Virtual Jaguar.app"(oropen -n -a "Virtual Jaguar" --args --data-dir DIR). On Windows and Linux just start the program again (a Linux AppImage starts itself again from$APPIMAGE). A copy started with--data-dirhands the same folder to the copies it opens. - Numbering. The first copy running on a data folder is instance 1 and behaves exactly as it always
did. The next ones are 2, 3, ... (the lowest number nobody holds; an OS file lock on
instance-N.lockin the data folder, released automatically when a copy quits or crashes). The number is in the window title ([Instance 2]). Copies with different--data-dirfolders, or a portable copy, share nothing and are all instance 1. - What each copy writes. Instance 2+ never rewrites the shared files:
| File | Instance 1 | Instance N (2+) |
|---|---|---|
config.ini (settings, bindings, core options, recents, RetroAchievements user name) |
read and written as before | read at start (so it has your settings); never written |
config-N.ini |
not used | its window size and its controller choice only |
imgui.ini |
imgui.ini |
imgui-N.ini |
game saves saves/<game>.srm (EEPROM, SRAM, Memory Track) and the GameDrive SD folder |
saves/ |
saves/instance-N/; a game's first run there starts from a copy of instance 1's save (read only) |
save states states/, cheat selections cheats/games/ |
shared | shared: written only when you press save, each file atomically, last save wins. A state saved in one copy loads in another |
| library cache, box art, database files | shared | shared, written atomically with a per-process temporary name |
favorites and recently played library/user.tsv |
shared | shared and written by every copy, but as operations: each change re-reads the file, applies that one change (star, play, remove, clear) and writes it atomically, so a copy never replays a stale snapshot over another's list; two changes in the same few milliseconds can lose one. Other copies pick a change up within a second |
library display options (library.* in config.ini) |
saved with the other settings | read, not saved (like every setting) |
netplay address (system/vj_netlink.txt) |
written when you join, as before | not written (no copy writes it): the address and port go to the core as per-process option overrides, so two copies can join different hosts |
Why: two copies both writing one .srm or one whole-file config.ini would silently lose
one side's changes. Settings you change in a second window are therefore not remembered, so make
them in the first window. Instance numbers are ordinal slots (the window you opened second),
not identities, which is why nothing else is persisted under them.
- Controllers. SDL gives every connected controller to every running copy, so two windows would both answer the same pad. Settings > Input > Devices & players > Controllers used by this window picks All controllers (the default), Keyboard only, or Controller k for each pad, remembered per instance number. Controller numbers follow the order SDL lists them (connection order). A window pinned to one controller keeps reading it while another window has focus (SDL otherwise ignores controllers in the background), so two players can play side by side; the keyboard goes to whichever window has focus. Which Jaguar port or player a controller drives inside a window is still chosen under Devices & players.
- Netplay on one machine: start a game in instance 1 and choose Netplay > Host; in instance 2 start the
same game and choose Netplay > Join with address
127.0.0.1and the same port. Two copies that both host need different ports (the window offers 42171-42174).
| normal | the OS per-user data folder (SDL_GetPrefPath): ~/Library/Application Support/VirtualJaguar/, %APPDATA%\VirtualJaguar\, ~/.local/share/VirtualJaguar/ |
| portable | data/ next to the executable, when a file named portable.txt sits beside it |
| override | --data-dir DIR |
Inside: config.ini (settings, core option overrides, recents), saves/
(<game>.srm: EEPROM, SRAM, Memory Track), states/ (<game>.slotN.state plus a
.png thumbnail), system/ (BIOS files, only if you want real ones), imgui.ini.
Emulation > Cheats... lists the cheats of the libretro cheat database for the running
game, with a toggle each, plus manual codes (hex ADDRESS:VALUE, + to join
several). What is on is remembered per game (<data>/cheats/games/<key>.ini, keyed
by the cartridge CRC32; discs by the core's title row or file name) and sent to the
core again after every load and state restore.
The cheat files are not committed to this repository (the libretro database is
CC BY-SA 4.0), but they are bundled with builds: configure downloads the 41
cht/Atari - Jaguar files of one pinned libretro-database commit (SHA-256 per file)
to <build>/cheats/, next to the program, and cmake --install puts them in
<prefix>/bin/cheats/ with the database LICENSE and a credit README beside them, so
the attribution travels with the files (a packaging step takes them from the install
tree). The download is fail-soft: with no network the configure still succeeds, the
cheats are then simply not bundled (never without their license). For offline or
distro builds: -DVJ_APP_FETCH_CHEATS=OFF. You can always add your own .cht files
to <data>/cheats/, or point the window's "Cheat library" box (or --cheats-dir) at
a folder of them. A set is picked by name from the core's own title (or the file
name); with several matches, or none, you choose. Jaguar cheats are credited to Gent
of Emucheats.
Database entries are RetroArch memory cheats (decimal address/value, width in
memory_search_size); they become the core's hex codes, value little-endian in
memory as RetroArch writes it. cheatN_enable is ignored: nothing starts on.
Achievements > Sign in... takes your RetroAchievements user name and password
once, gets a login token from the server and keeps only the token (the password is
wiped from memory as soon as the request is built). The user name is ra.user in config.ini;
the login token is kept in the system keychain (macOS Keychain, Windows Credential Manager, or
the Linux Secret Service through libsecret, loaded at run time) and not in config.ini. A token
that an earlier version left in config.ini is moved into the keychain at start-up. Where no
keyring is available (no libsecret, no running keyring service, a Flatpak without the secret
portal) the token stays in config.ini as ra.token, Settings says "Token stored in the settings
file because no system keyring is available", and that file is then a credential: treat it like a
password file. Sign out deletes the token from the keychain (and from config.ini). A second copy
of the app (see below) reads the keychain and writes it only when you sign in or out there. The menu also has the hardcore toggle, "include unofficial achievements"
and the Achievement list, a dashboard with your avatar, badges and the Jaguar games that
have achievements (see below). Unlocks, game completion,
leaderboard attempts / submissions, progress indicators and server problems show as
toasts.
It is built on rcheevos' rc_client (v12.5.0, MIT, fetched at configure time like SDL3):
- Game identification. rc_client hashes the file itself: the whole cartridge file for
Atari Jaguar (console 17), the second-session boot executable for a
.cuedisc (Atari Jaguar CD, console 77, rcheevos' built-in cue/bin reader)..chdand.cdidiscs are not identified yet: rcheevos' built-in reader covers cue/bin only and this app has not supplied a CHD/CDI reader (RetroArch installs its own CD reader). The game runs and the app says why. A.binfile is a cartridge for this core and is hashed as one. - Memory. The core publishes a memory map (
RETRO_ENVIRONMENT_SET_MEMORY_MAPS: 2 MB system RAM at$000000). rcheevos'rc_libretro.c, the same code RetroArch builds, maps addresses onto it; nothing is byte-swapped. The RAM is a big-endian image and Jaguar sets use the big-endian operand sizes (0xI16-bit,0xG32-bit). - Hardcore. While signed in, hardcore is on and a game is being tracked: loading
save states is refused (saving works), pausing is rate-limited (
rc_client_can_pause), enabling it mid-game (or signing in mid-game with it on) restarts the game for a clean run, core options that rcheevos disallows (todayvirtualjaguar_pal = enabled) pause hardcore, andAchievements::allowCheats(),allowRewind(),allowSlowMotion()andallowFrameAdvance()are false so those features (cheats, rewind, slow motion and frame advance are separate pieces) must ask. Fast-forward is allowed (it is not slower than real time). Softcore (the default) restricts nothing, and each state file gets a.rcpsidecar so a restored state does not replay stale hit counts. - Cheats. No cheat reaches the core while hardcore is active: the cheat manager's
apply()consultsAchievements::allowCheats(), so loads, state restores, the window, manual codes and--cheat/--cheat-onall obey it. Entering hardcore empties the core's list, leaving it (softcore, sign-out, RetroAchievements off) puts the saved per-game selections back; they are never changed on disk meanwhile. The Cheats window stays openable with its controls disabled and says why. - Network. libcurl (found at configure time;
libcurl4-openssl-devon Linux) or WinHTTP on Windows, on one worker thread; replies are handled on the main thread, so a slow network never delays a frame. Without libcurl the app still builds and the Achievements menu says it is unavailable. Unlocks are retried by rc_client when the network drops. - Pictures. Your avatar (
rc_client_user_get_image_url), the game icon (rc_client_game_get_image_url) and every achievement's badge, earned or greyed (badge_url/badge_locked_url), show in the Achievements window, the menu and the unlock toasts. They download on a worker thread of their own (so they never queue behind rc_client's requests) and are kept inra-cache/in the data folder: a badge never changes, so a saved file is reused forever; the avatar is saved peravatar_last_updated. The folder is kept under 48 MB (oldest pictures go first) and at most 256 are held as textures at once (least recently drawn goes first; it comes back from disk when asked for again). They follow the Library's download art switch: with it off, or with Achievements > Download pictures unticked, only pictures already saved are shown and nothing is fetched. Headless runs and--ra-mocksessions never fetch. - Dashboard. Achievements > Achievement list... is a window with three tabs.
This game: icon, title, hardcore / softcore rule in force, progress bar, and the achievement
list with badges, points, rarity, earned date and a Hardcore / Softcore marker, with a filter
(all, unlocked, locked, unlocked in hardcore, missable), a sort (default, title, points, rarest,
recently unlocked, type) and a search box. Jaguar games: every Atari Jaguar (console 17) and
Jaguar CD (console 77) game that has achievements (
rc_client_begin_fetch_game_list), with icon, achievement count, points and your progress in each (rc_client_begin_fetch_all_user_progress, signed in only); filter by all / in my library / in progress / completed / not started. Games in your library get a Play button: cartridges are matched by the MD5 RetroAchievements lists for the game (hashed on a background thread), everything else by title against the library's names. The list is saved inra-cache/games-<console>.tsvand refreshed when it is a day old (and only while downloads are on); Refresh fetches it again on request. Profile: avatar, name, hardcore and softcore points, and what you unlocked in this session. rc_client has no call for the account's older unlock history (that needs the web API and its key), so it is not shown. Neither list needs a web API key: both use the login token like the rest. - Tests never touch the network or an account:
--ra-mock DIRanswers rc_client's requests fromapp/tests/ra/<api>.json(<api>_<console>.jsonfirst forsystemgamesandallprogress, one list per console).vj_ra_dash_testcovers the picture cache (paths, disk, offline mode, LRU), the games-list parsing and library matching, the cache file, and the filters and sorts;ra_dashboard_mockfetches the lists through the mock server. Seecmake/VJAchTests.cmake.
The Netplay menu opens a window for the link-cable games (Doom deathmatch, BattleSphere,
AirCars, and Ultra Vortek over the Voice Modem) between two computers on a local network (or two windows on one machine, see
Running several instances). It
configures the core's existing link networking (docs/netlink-user-guide.md) through core options
and takes status from the core's event channel (see Core events below; a core that never answers
falls back to reading its [NETLINK] log lines); it is not rollback netplay, and play over
the Internet or a VPN is not supported. Both players need the same game and the same version of
the app, and the same device (JagLink / CatBox cable or Voice Modem).
- Host a game lists this machine's LAN addresses and starts listening on a port: one of the core's four presets (42171-42174) or any port from 1 to 65535 under Custom....
- Join a game finds hosts on the LAN (the core's UDP beacon) or takes a typed host name or IPv4 address, set straight on the core's host option (the core resolves names itself).
- Mode, host and port are session overrides and are never saved, so relaunching the app does not start listening on its own; the link device is an ordinary saved option.
- Command line twin:
--netplay host|join|join-discovered|browse|loopback,--netplay-addr,--netplay-port,--netplay-device jaglink|voicemodem. Advanced > Echo test runs the link against itself with no partner.
What the app does so it behaves like a program of the OS it runs on (#833). The code is behind one
interface, src/platform/platform.h; every OS-specific file is small and a platform with nothing to
add compiles a no-op, so adding a platform feature never touches the others.
| macOS | Windows | Linux | |
|---|---|---|---|
| Menu bar | native, with Cmd shortcuts (the in-window bar is hidden) | in-window | in-window |
| Dock / taskbar menu | Dock menu: recent games, library, open, pause, reset | Jump List: recent games | desktop Actions: Open Game Library, Boot Without Cartridge |
| Recent games | NSDocumentController (Dock, Open Recent) |
Explorer Recent and the Jump List (SHAddToRecentDocs) |
not written (no GTK; see below) |
| File types (double-click, Open With, drop on the icon) | CFBundleDocumentTypes + an exported type for .j64 / .jag |
installer task: per-user OpenWithProgids (and the default for .j64 / .jag) |
shared-mime-info XML + MimeType= |
| Window | fullscreen follows the green button / Ctrl+Cmd+F; App Nap held while a game runs | title bar follows the app theme (dark or light), per-monitor-v2 DPI, long paths, one taskbar button with the installer shortcut (AppUserModelID) | the application id is the Wayland app_id / X11 WM_CLASS, so the icon and "pin to dock" match the .desktop file |
| Tray / menu-bar icon | optional (Settings > Storage > Desktop) | optional | optional where the desktop has a StatusNotifier host (KDE and others; stock GNOME needs an extension) |
| Screensaver / display sleep | allowed in the library and while paused, blocked while a game runs | same | same (through the portal inside Flatpak) |
| Show in Finder / Explorer | File > Show in Finder | - | opens the folder |
One model, many surfaces. src/platform/commands.cpp builds the menus (File, Emulation, View,
Achievements, Netplay, Window, Help, the tray and the Dock menu) from a snapshot of the app state, and
every click is posted as an SDL user event and run by the main loop (App::runNativeCommand), never
inside Cocoa's menu-tracking loop or SDL's tray callback. The macOS menu bar is built after SDL_Init
over SDL's default one and leaves SDL's app delegate alone (it turns Finder's "open" into the drop event
the app already handles). Only Cmd-modified shortcuts are bound there; F5 / F7 / F8, Tab and the other hotkeys
stay with the app. While a native menu is open the macOS event loop is blocked by Cocoa, so emulation and
audio pause for as long as the menu is held open.
File types. The associated extensions are .j64 .jag .rom .abs .cof (games) and .cue .chd .cdi
(discs). .bin and .prg open through File > Open and drag and drop but are too generic to claim. The list lives
in src/platform/commands.h (VJ_ASSOC_LIST); the CTest platform_assoc_sync fails when the Info.plist,
installer, .desktop, mime XML or the application id drift from it. On Windows and Linux a double-clicked
game starts a new process, which is a second instance when the app is already running (see "Running several
instances": its own save folder, settings not kept); on macOS the running copy opens it.
virtualjaguar --dump-native-menu (macOS) prints the real menu bar and Dock menu and checks every item;
exit 77 means no window-server session (ssh) or not macOS.
Windows outside the main window, and the in-window menu bar. Dear ImGui is the -docking build of the same
version (v1.92.8-docking; pinned in CMakeLists.txt and the Flatpak manifest), used only for its multi-viewport
mode: Settings > Storage > Desktop > "Let windows move outside the main window" lets a Settings / Achievements /
Cheats / Netplay / debug-tool window dragged past the edge of the main window become an OS window of its own
(real title bar, no Dock / taskbar entry, positions kept in imgui.ini). Default on for macOS and Windows, off on
Linux; it is forced off on Wayland (a client cannot position its own extra windows there; X11 and XWayland are
fine), while the window is fullscreen, and in headless runs (src/platform/viewports.*, unit-tested in
platform_unit). Docking is left off: it would put tab bars and drop targets on windows people already arranged.
The coordinates of the main window are screen coordinates while this is on, so code that mixes them with SDL's
window-relative pointer events subtracts GetMainViewport()->Pos. On macOS, "Show the menu bar inside the window"
(ui.menubar_in_window, off by default; F1 flips it) keeps the ImGui menu bar as well as the native one.
Settings > Discord shows what you are playing on your Discord profile (Rich Presence), and the
activity doubles as app discovery: friends see "Playing Virtual Jaguar", the game's box art, and a
Get Virtual Jaguar button that opens the site's download page (https://jaguar.provenance-emu.com/app.html).
Discord has to be running on the same computer; nothing is sent anywhere else, and nothing is installed (the app
speaks Discord's local RPC protocol itself, no Discord SDK).
- While playing: the title (dat name without region and revision tags) as the headline; the line below says
Atari JaguarorJaguar CDplus| Paused,| Netplay: hosting / joined(never an address),| Hardcoreand| 12/30 achievementswhen a RetroAchievements game is tracked; the elapsed time counts from boot. The large picture is the game's box art, a public https address (libretro-thumbnails, or the project site's Jaguar CD mirror) that Discord's media proxy fetches. It is used only when the library already downloaded that picture, so Discord never shows a broken image; otherwise the application'sjaguar(cartridge) orjaguarcd(disc) asset is shown. The small picture is the app icon (app) with "Virtual Jaguar ". - In the library or an empty window: "Browsing the library" with the
librarypicture. - Buttons: Get Virtual Jaguar, and View on RetroAchievements (the game's page) while a RetroAchievements game is tracked. Discord shows buttons to other people only, so your own profile never shows them.
- Settings: Show what I'm playing on Discord (default on), Show game artwork, Show buttons, and the
connection status ("Connected to Discord", "Discord is not running"). Config keys
discord.enabled,discord.art,discord.buttons. - Application ID: the default is the project's "Virtual Jaguar" Discord application, set by the CMake cache
string
VJ_DISCORD_APP_ID(-DVJ_DISCORD_APP_ID=empty turns the feature off; a fork passes its own). The config keydiscord.app_id(Settings > Discord > Application ID) overrides it. - Several windows: only the first copy (instance 1) publishes, so two windows never fight over one status (see Running several instances).
- Behaviour: the connection and every write live on a background thread; if Discord is not running the app looks again every 15 seconds and never waits on it, quitting takes the status down within a quarter of a second, and updates are sent only when something changed and at most 4 per 20 seconds (Discord allows 5).
- Create an application named exactly Virtual Jaguar (Rich Presence cannot set the name; this is what people
see after "Playing"). Its Application ID is the number in
VJ_DISCORD_APP_ID. - Upload these art assets under Rich Presence > Art Assets, with exactly these lowercase keys (at least 512x512):
jaguar(cartridge fallback large image),jaguarcd(Jaguar CD fallback),library(browsing),app(the app icon). - CI: the repository variable
DISCORD_APP_ID(Settings > Secrets and variables > Actions > Variables) overrides the CMake default inapp-package.yml; leave it unset to use the default. An application ID is public, not a secret. - Test from a second Discord account: buttons are hidden on your own profile.
Tests: ctest -R discord (frame encode/decode, JSON escaping, socket paths per OS, the rate limiter, the activity
built from app state, and a loopback test against a fake Discord server on a Unix socket; skipped on Windows).
The core publishes every option as a list, but for a handful it reads any value, so Settings >
Core options shows the core's presets plus a Custom... entry that opens a box (applies on Enter
or when it loses focus; an invalid value says why in red and is not applied). The keys are in
src/typed_options.h, with the range the core actually honours: link port, GDB port (1-65535),
GDB halt timeout (1-3600 s), mouse / rotary sensitivity (1-1600%), the per-axis dead zone (0-64),
offset (-64..64) and response curve (1-800%), the link host (host name or IPv4, 1-127
characters, no IPv6: the link sockets are IPv4), and the M68K / RISC clock scales (a percentage,
25-400, e.g. 175%). Saved values and --option K=V use the same validators, so
--option virtualjaguar_netlink_port=50123 works; every other option stays a strict list.
Tested by typed_options (validators, CoreHost, and the real core reading a custom port, host and
clock scale).
The core reports link up/down, link statistics, wire speed and crash-watchdog hits to the app
through a private libretro environment call that only this app answers
(src/core/vj_frontend_ext.h, docs/app-frontend-extension.md); under RetroArch it is a no-op.
CoreHost answers the handshake and hands each event to onEvent; Netplay consumes them instead
of scraping log text (the log parser remains the fallback), and a crash-watchdog hit shows as an
on-screen message naming the signature.
The Tools menu is a set of developer windows over a private debug interface the core hands only to this app
(src/debug/vj_debug_iface.h, docs/app-frontend-extension.md); RetroArch never sees it. Full reference:
docs/app-debug-tools.md.
- Memory viewer (hex / ASCII grid over every bus region, go-to, search, change highlighting, edits, "Add as cheat") and Palette (CLUT) (256 swatches as CRY or RGB16, with an editor).
- Cheat scanner: find an address by narrowing (changed / increased / equal to ...), then keep it as an ordinary cheat.
- Object list: the whole display list the Object Processor walks, decoded, with a bitmap preview, an outline over the game picture, PNG export and a pixel painter (RAM only).
- Blitter log: the last 256 blits with their decoded command and registers.
- Textures: browse the core's texture dump, export a tile, replace its art with your own PNG.
- Audio scope: waveforms and peak meters of the audio the core produces.
- F9 breaks into the game (continue when halted), F10 steps while halted; a "HALTED" bar shows over the picture. The CPU debugger window is still to come.
Writes go to RAM freely; hardware registers need "Allow register writes"; the CD registers ($DFFF00-$DFFFFF) and
B_CMD, G_CTRL, D_CTRL, OLP, LTXD, RTXD are never written from here. Everything greys out (and closes) while
RetroAchievements hardcore is on. Extra app instances keep their own windows but do not dump textures. Developers can
open one at start-up: --show-window memory|palette|scanner|objects|blitter|textures|scope (with --screenshot).
virtualjaguar --headless runs with SDL's dummy video and audio drivers: no
window, no GL. Each game is delivered as a drop event (the same handler the
window uses), run for --frames frames, and reported as an FNV-1a frame hash
(the same hash test/harness/harness.c uses). CTest runs:
-
smoke_yarc: boottest/roms/yarc.j64under HLE with no BIOS, check the hash. -
smoke_reload: yarc, jagniccc, yarc in one process; the two yarc runs must match, which proves a different game in between left no static state behind. The number of visible core options must match too (it differs between a cartridge and a disc, so a stale visibility table would show). -
smoke_state: save state and thumbnail, run on, load, run on; both runs match. -
smoke_bad_option: an--optionthe core does not register is rejected, not silently ignored. -
smoke_cd: load the synthetic CD image by path (no BIOS). -
smoke_boot_cd_bios/smoke_boot_cart_rom: boot the CD BIOS with no disc, and the boot ROM with an empty slot, from the images built into the core (no files needed); both must draw (no blank frame) and match a hash. -
bios_unit(vj_bios_tests): scanning BIOS files in nested folders with upper and mixed case names, what may be chosen (size), what Boot does and why not, the saved choice, importing (reuse, never overwrite, refusals), and how the running BIOS is described. Synthetic files only. -
bios_e2e(vj_bios_e2e): the real core and the private BIOS channel: a chosen CD BIOS / boot ROM file is the image that runs (checksum, source), a refused or vanished file falls back, a frontend without the capability is never asked, and a no-content boot enters the boot ROM or the CD BIOS as asked. -
library_unit(vj_library_tests): dat parsing, CRC identification of synthetic files (copier-headered and plain carts, a.cueset), thumbnail naming against known libretro examples, cache paths, a scan of a temp folder with the hash cache, and on POSIX the whole art pipeline (scan, identify, download, cache, decode, texture; database update; then offline) against a loopback HTTP server; favorites / recents (persistence, ordering, matching by path, checksum and name, the shared file under two simulated copies), the favorites / recents display options and tab layout, and save-state naming, listing and deletion over synthetic state files. No internet, no GL.vj_library_tests --scan DIR --db DIRprints identification counts for a folder (dev tool). -
remap_unit: the remapping logic with no core (bindings, precedence, config round trip, keypad translation, overlay, bind capture, content identity). -
input_e2e: drives the real core with every Jaguar control on both ports and reads back the console's joypad matrix; keypad 7-9,*,#included. -
devices_unit/devices_e2e: the non-pad devices (kinds, per-game persistence, Team Tap player slots, light-gun geometry, mouse-capture policy, locked core options, the Pro Controller keyboard layout, the controls cheat sheet and its auto-show rule), then the same host-input routing driving the real core and reading back the mouse / rotary quadrature state, analog / driving / 6D values decoded off$F14000as a game does, the paddle ADC at$F17C00, the light gun's aim point and the Team Tap socket slots. Every group has a negative control (no host input reads the rest value). -
theme_unit(vj_theme_test): the theme choice (System / Dark / Light, the OS's answer, the theme-changed event) and WCAG contrast of every palette and style colour in both themes, and of the in-game overlays on white and black frames; the custom-theme files (round trip, import validation, the themes folder, read-only secondary copies) and the editor's contrast warnings. -
controls_overlay_*: the overlay through the real app: hidden by default (with the F3 hint), shown for a keyboard player when the setting is on, hidden when it is off, text follows a saved rebinding.devices_unitcovers the show / hide policy (click, tap, Esc, F3, timeout). -
debug_*: the debug tools.debug_session_unitand the per-tool unit tests (debug_memory_unit,debug_scan_unit,debug_op_unit,debug_texture_unit,debug_blitter_unit) run the pure logic against a fake core (tests/debug_mock.h);debug_memory_ui_smokeanddebug_scan_ui_smokedraw the windows headlessly over it;debug_e2e,debug_scan_e2eanddebug_op_e2edrive the real core onyarc.j64(halt / step / continue, a found counter kept as a working cheat, the display list captured by the core).
To regenerate the expected hash after an intentional emulation change:
virtualjaguar --headless --frames 300 test/roms/yarc.j64 and update
VJ_SMOKE_YARC_HASH in app/cmake/VJSmokeTests.cmake.
Settings > Video > Appearance picks the colour theme: System (the default) follows the OS and switches live when it does, or Dark / Light / a custom theme made in Customize... and kept as <data>/themes/<name>.ini (--theme system|dark|light|custom:NAME sets it for one run); the palettes are in src/theme.cpp.
The UI scale follows the display's content scale (200% on a 4K Windows/Linux display; 1.0 on a Retina
Mac, where ImGui renders at the pixel density by itself). ui.scale in config.ini (default 1.0) multiplies it.
Settings > Video > Font picks the typeface and its size (11-24 pt, default 13): Retro (Dear ImGui's embedded
pixel font, the look the app always had, still the default), System UI (San Francisco on macOS, Segoe UI on
Windows, the desktop's sans-serif on Linux through fc-match, with DejaVu / Noto / Liberation as fallbacks),
Monospace (SF Mono / Menlo, Consolas, DejaVu Sans Mono) or Custom file... (a .ttf / .otf / .ttc; it
is checked, then copied to <data>/fonts/). Changes apply at once and persist as ui.font.* in config.ini.
Text is rasterised at the display's pixel density (Dear ImGui 1.92's dynamic atlas), so it is crisp on Retina and
at 125-200% Windows scaling; it is grayscale anti-aliased with sub-pixel glyph positioning. LCD (RGB) sub-pixel
anti-aliasing is not possible: the glyph atlas is a single coverage channel (and macOS dropped LCD smoothing in
Mojave). A missing or unreadable font falls back to Retro with a note in the Font section. Debug windows use
fonts::mono() / fonts::pushMono() (src/fonts_imgui.h), a monospace face whatever the UI font is. Glyphs the
chosen font lacks (arrows, star, check mark) come from a symbols font or the built-in vector face.
FreeType is the default renderer (FreeType License, fetched and pinned, built static) and provides the
Hinting (none / light / full) and Smooth edges choices. -DVJ_APP_FREETYPE=OFF builds with Dear ImGui's
stb_truetype instead, which hides those two choices; the Retro pixel font always uses stb_truetype.
Every window opens at a size in font heights (so it scales with DPI), at most 80% of the main
window, is constrained to 95% of it each frame (a size saved in imgui.ini by a bigger window never
overflows a smaller one) and is pulled back on screen. Long text wraps, tables stretch, controls take
a share of the width that is left (src/ui_util.h). To check a layout:
# window size comes from window.w / window.h in <data>/config.ini; --ui-scale 2 stands in for a HiDPI display
virtualjaguar --data-dir /tmp/d --ui-scale 2 --screenshot out.png --screenshot-frame 40 \
--show-window settings-input game.j64--low-dpi opens a 1x window even on a Retina display (screenshots of a standard-density screen).
--show-window takes settings[-video|-input|-audio|-storage][:FILTER], states, about,
cheats, library, login, achievements, keypad and theme-editor.
| file | job |
|---|---|
cmake/VJCore.cmake |
core + libchdr static libraries from Makefile.common |
cmake/VJSmokeTests.cmake |
CTest smoke tests |
src/core_host.* |
libretro host: environment calls, options, SRAM, states, discs, the core's private event channel |
src/typed_options.h, src/typed_ui.h |
which options take a typed value and their validators; the Custom... combo and box |
src/app.* |
window, GL, main loop, pacing, events, save-state slots, headless runner |
src/video.* |
XRGB8888 texture and presentation (plain, or through the shader chain) |
src/present_util.* |
GL-free: where the picture goes (aspect / integer / stretch), .slangp parsing and folder scan |
src/shaders.*, src/shaders_ui.cpp |
librashader loader and GL filter chain; the Shader section of Settings > Video |
shaders/ |
first-party presets copied beside the executable |
cmake/VJShaders.cmake |
pinned header fetch, optional prebuilt runtime, shader tests |
src/audio.* |
SDL3 audio stream with dynamic rate control |
src/input.* |
keyboard and gamepad snapshots to libretro input |
src/remap.*, src/remap_ui.cpp |
profiles, per-game overrides, keypad delivery, overlay, bind capture, and their UI |
src/devices.* |
device kinds, per-port / per-game selection, player slots (Team Tap), light-gun geometry, mouse-capture policy, locked core options |
src/router.* |
host input to what the core reads for every device (SDL-free, tested against the real core) |
src/devices_ui.* |
Settings > Input > Devices & players, claiming devices with the core, per-frame mouse capture |
src/cheatsheet.*, src/cheatsheet_ui.cpp |
the controls cheat sheet: text, auto-show rule, window |
src/ui.* |
ImGui: menu bar, settings, options menu, state manager, OSD |
src/fonts.* |
UI fonts with no ImGui: ui.font.* settings, per-OS system font lookup, custom file validation + import, glyph ranges (unit tested) |
src/fonts_imgui.*, src/fonts_ui.cpp |
the live font atlas (typeface, size, FreeType flags, mono()), and Settings > Video > Font |
src/platform/ |
native-OS integration: menu model + dispatch (commands.*, no OS), per-OS files (platform_macos.mm, platform_windows.cpp, platform_other.cpp), tray (tray.cpp); src/desktop.cpp is the app side |
src/library.* |
game library logic: dat parser, CRC identification, folder scan + hash cache, libretro-thumbnails names |
src/library_lists.* |
favorites, recently played, display options, tab layout, save-state listing (pure) |
src/library_service.* |
scan / decode / download worker threads, art cache and texture handover |
src/library_ui.cpp |
the Game library: the home screen and the window |
src/ui_util.h |
layout helpers every window uses: sizes in em clamped to the main window, width fractions, wrapped rows |
src/http.* |
blocking HTTP(S) GET: libcurl, WinHTTP or nothing |
src/cheats.* |
.cht parsing, conversion to core codes, per-game cheat state (no SDL, unit tested) |
src/cheats_core.cpp, src/cheats_ui.cpp |
core glue (retro_cheat_*, title identity, CLI) and the Cheats window |
cmake/VJCheatData.cmake, cmake/VJCheatManifest.cmake, cmake/VJCheatTests.cmake |
pinned optional database download, cheat tests |
src/paths.*, src/config.* |
data folders (portable mode) and the settings file |
src/achievements.* |
RetroAchievements: rc_client glue, memory, hardcore policy, mock server for tests |
src/achievements_ui.cpp |
ImGui: sign-in, the dashboard window (this game / Jaguar games / profile), toasts, trackers |
src/achievements_games.cpp |
dashboard data: picture URLs, achievement rows, the games list and progress, library matching |
src/ra_dash.* |
dashboard logic with no ImGui or network: games parsing and matching, cache file, filters and sorts |
src/ra_images.* |
the avatar / icon / badge cache: disk files, bounded textures, offline mode |
src/ach_http.* |
libcurl / WinHTTP worker for rc_client's requests |
cmake/VJRcheevos.cmake, cmake/VJAchTests.cmake |
rcheevos fetch + static lib; headless achievement tests |
General rollback netplay for any game (#864); the Netplay window covers the link-cable games only. Microphone and netpacket are reported unsupported.