# The MIT License (MIT) # # Copyright (C) 2026 Scott Duensing # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. .section Technical Reference (for Developers)|Libraries .topic lib.joystick .title Joystick Library .toc 0 Joystick Library .index joystick .index joystick.h .index joyCount .index joyGetInfo .index joyOpen .index joyOpenShared .index joyClose .index joyRead .index joyButtonPressed .index joyButtonWentDown .index joyBasStick .index STICK .index STRIG .index joySetEventFn .index joyPump .index joyRescan .index joyGetRaw .index joyGetCalibration .index joySetCalibration .index joyGetDeadZone .index joySetDeadZone .index joySettingGet .index joySettingSet .index joySettingsLoad .index joySettingsStore .index joyAxisScale .index joyDeadZoneApply .index JOY_DRIVER .index JoyStateT .index JoyInfoT .index JoyEventT .index JoyRawT .index JoyDriverT .index JoyAxisCalT .index joystick.bas .index game port .h1 Joystick Library Game controllers for DVX programs behind one API, whatever the hardware: the devices the drivers found, axes calibrated to -32768..32767 with a dead zone round the centre, buttons, hats, read when the program wants (polled) or delivered as events. A program's handles close when it ends, however it ends. Calibration and the other settings live in DVX.INI [joystick]; the Control Panel's Game Controllers page sets them. Header: joystick/joystick.h Loaded as: bin/libs/kpunch/joystick/joystick.lib, drivers in bin/libs/kpunch/joystick/drivers/*.drv (joyport.drv, joykeys.drv), loaded only when a program first asks for devices. .h2 Reading a Joystick .code if (joyCount() > 0) { int32_t stick = joyOpen(ctx, 0); // device 0, owned by the calling program JoyStateT st; if (joyRead(stick, &st) && st.connected) { shipX += st.axes[0] / 4096; // -8..7 firing = (st.buttons & 1) != 0; } } .endcode joyCount() is the number of devices (the first call of any joystick function finds them); joyGetInfo(device, &info) describes one: name, id (the driver's name and unit number, "gameport0", which prefixes the device's DVX.INI keys), axisCount, buttonCount, hatCount, axisPresent (bit n set when axis n answers; a missing axis reads 0) and caps (JOY_CAP_CALIBRATE for analog axes, JOY_CAP_KEYBOARD for the keyboard joystick). joyOpen(ctx, device) returns a handle owned by ctx->currentAppId, or -1. joyOpenShared(ctx, device) returns the calling program's shared handle on the device, opening it the first time (for code with no place to keep a handle; the BASIC functions use it). joyClose closes a handle; a program's handles are closed for it when it ends. joyRead(handle, &st) fills a JoyStateT: axes[] (JOY_AXIS_MIN..JOY_AXIS_MAX, 0 inside the dead zone), hats[] (JOY_HAT_CENTERED or degrees clockwise from up), buttons (bit n = button n) and connected (false when the device stopped answering; everything then reads neutral). A button that went down and up again since the handle last looked reads as down once, so a quick tap between two reads is not lost. joyButtonPressed(handle, button) asks the same about one button. joyButtonWentDown(handle, button) only asks whether the button went down since the last time this function asked about it (a press counter of its own, apart from joyRead's). The keyboard joystick (JOY_CAP_KEYBOARD) reads neutral for a handle whose program has no window with the focus, so keys typed into another program do not steer the game. .h2 Events joySetEventFn(handle, fn, userData) delivers the device's changes from now on: JoyEventAxisE when an axis moved by at least JOY_EVENT_AXIS_STEP or reached the centre or an end, JoyEventButtonDownE / JoyEventButtonUpE (a tap between two polls gives both), JoyEventHatE. fn runs in program context as the handle's program (ctx->currentAppId is its id), so DVX calls are fine. Events are delivered by joyPump, which the shell calls on every idle frame; a program with its own main loop that repaints every frame (so the shell is never idle) calls joyPump itself or reads with joyRead. joyPump is not reentrant: an event function that runs a modal dialog gets its remaining events after it returns. A NULL fn stops the events. .h2 Polling and CPU Cost While at least one handle is open, a timer-service callback marks a poll due [joystick] pollHz times a second (JOY_POLL_HZ_DEFAULT 60, JOY_POLL_HZ_MIN 10 to JOY_POLL_HZ_MAX 200) and runs the drivers' tick hooks (the game port latches button presses there). The poll itself runs in program context, the next time a program reads (joyRead, joyButtonPressed, joyGetRaw) or the shell is idle, so however often a program reads there are at most pollHz polls a second. With no handle open nothing is polled and the timer is removed. A game port poll waits for the slowest connected axis to run out: about 0.6 ms with the stick centred, up to about 1.2 ms at an end. At 60 polls a second that is about 4 to 7 percent of the processor's time on any machine, whatever its speed, while a program has a joystick open; the timer interrupt's part (one port read) is negligible. The keyboard joystick costs nothing measurable. .h2 Calibration and Settings joyGetRaw(device, &raw) returns the last raw poll (driver units: microseconds for the game port). joyGetCalibration / joySetCalibration get and set an axis's JoyAxisCalT (min, center, max in raw units; min < center < max): min..center maps to -32768..0, center..max to 0..32767, clamped. A NULL calibration forgets it. An axis never calibrated ranges automatically: centred on the reading taken when the device was found, starting at center / 8 .. center * 2 and widening as the stick moves. joyGetDeadZone / joySetDeadZone get and set the device's dead zone in percent (0..JOY_DEADZONE_MAX, default JOY_DEADZONE_DEFAULT 8). joyAxisScale(cal, raw) and joyDeadZoneApply(value, percent) are the two steps as plain functions. Settings changed through the library take effect at once and reach DVX.INI only through joySettingsStore(prefs) (the caller saves the file); joySettingsLoad(prefs) replaces them all, e.g. to undo. joySettingGet / joySettingSet read and write single values. The [joystick] keys: .table Key Value --- ----- pollHz Polls a second (10 to 200, default 60). fallback auto (default): the keyboard joystick only when no other device is found; always; never. gameport.mode twoSticks (default) or fourAxes: one joystick using all four axes and buttons (flight sticks with a throttle or rudder). keyboard.up ... keyboard.right, keyboard.button1 ... keyboard.button4 Key names (joyKeyName: Up, Down, Left, Right, LCtrl, RCtrl, LShift, RShift, LAlt, RAlt, Space, Enter, Tab, Backspace, Insert, Delete, Home, End, PgUp, PgDn, Keypad0 to Keypad9, A to Z, 0 to 9). Defaults: the arrows, Left Ctrl, Space, Left Shift, Z. .axis min,center,max: axis n of the device with that id, e.g. gameport0.axis0 = 40,560,1150. .deadZone The device's dead zone in percent. .endtable joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise. .h2 The Game Port Driver joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched. In the default mode the port makes up to two joysticks (A with axes 0-1 and buttons 0-1, B with axes 2-3 and buttons 2-3; either may be missing); in the four-axis mode one joystick with four axes and four buttons. .h2 The Keyboard Joystick joykeys.drv is a fallback driver: a joystick made of keys (two axes: full deflection while a key is held, the centre with both or neither; four buttons), read from the held-key table, so games work without joystick hardware. It is listed only when no other device is found, unless [joystick] fallback says always or never. .h2 Writing a Driver A driver is a driver module (see dvxDrv.h in the API reference): a file .drv in bin/libs/kpunch/joystick/drivers, built by the joystick Makefile from each drivers// folder, that exports one JoyDriverT named joyDriver, written with JOY_DRIVER(name, priority, .flags = ..., .probe = ..., .info = ..., .poll = ..., .tick = ..., .settingsChanged = ...). Nothing lists drivers and nothing loads them at startup: the first time a program asks for devices (the GUI and the timer service are running by then), and at every joyRescan, the library loads them all, probes them from the highest priority down (JOY_PRIORITY_PORT, ..., JOY_PRIORITY_KEYBOARD), probes the fallback drivers only when the fallback setting asks for them, and unloads every driver that found no device. The descriptor carries DVX_ABI_VERSION; a driver built for another version, one without probe, info or poll, or one whose name a higher priority driver already has is skipped and logged. .table Member Meaning ------ ------- hdr.name Lower case, no blanks ("gameport"); the prefix of the device ids and of the driver's own [joystick] keys. hdr.priority Probing order (JOY_PRIORITY_*); devices are numbered in that order. flags JOY_DRIVER_FALLBACK for a stand-in like the keyboard joystick. probe() Find the units; return how many. Called right after the driver is loaded. info(unit, out) Describe one unit (name, counts, axisPresent, caps); the library fills in id. poll(raw, units) Read every unit now, in program context: raw[0..units-1]. Set axisValid for each axis measured (the others keep their last value), buttons held now, pressed for buttons that went down since the last poll, hats (JOY_HAT_CENTERED when not pushed) and connected. Axes are raw units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN..JOY_AXIS_MAX. tick() NULL, or called INSIDE THE TIMER INTERRUPT at the poll rate while a handle is open. The timer service's rules apply; the driver pins the code and data it touches with platformLockRegion and releases them with platformUnlockRegion when its module is unloaded (a destructor), so rescans do not pile up locked memory. settingsChanged() NULL, or re-read the driver's settings (joySettingGet) after they changed. .endtable Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded again at every probe, so it must not rely on state kept from an earlier load; set its hardware up in probe (or a constructor). .h2 BASIC src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton and JoyHat. Joystick, axis, button and hat numbers start at 1; each function reads through the calling program's shared handle. The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.