commit 75ff3849bff7309a164a84664f7e916409d0cdc4 Author: Scott Duensing Date: Tue Oct 13 13:54:56 2026 -0500 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a07f470 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Build outputs (make) +obj/ +bin/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..913c9b4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30db38e --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +# Makefile -- KPJOY.DRV, built on Linux with Open Watcom v2's 16-bit Windows +# target (the copy JoeyLib's installer sets up, unless WATCOM names another). +# +# make bin/kpjoy.drv +# make LOG=1 the same, logging to C:\KPJOY.LOG (see kpjoy.c) +# make clean remove obj/ and bin/ +# make OBJ=DIR BIN=DIR build somewhere else (an installer's build tree) +# +# Compiler flags: Windows target (-bt=windows) DLL (-bd), small model with +# SS != DS (-ms -zu: a driver runs on its caller's stack), Windows prolog +# for exported functions (-zw), no stack probes (-s), 286 code (-2), +# structures packed on 2 bytes as the Windows headers are (-zp2), all +# warnings, as errors (-w4 -we). + +WATCOM ?= $(abspath $(CURDIR)/../joeylib/toolchains/opt/openWatcom) +export WATCOM +export PATH := $(WATCOM)/binl64:$(PATH) + +CC := wcc +LINK := wlink +LOG ?= 0 +CFLAGS := -q -bt=windows -bd -ms -zu -zw -s -2 -zp2 -os -w4 -we -i=$(WATCOM)/h -i=$(WATCOM)/h/win -DKPJOY_LOG=$(LOG) + +OBJ ?= obj +BIN ?= bin +TARGET := $(BIN)/kpjoy.drv + +all: $(TARGET) + +$(OBJ)/kpjoy.obj: kpjoy.c Makefile | $(OBJ) + $(CC) $(CFLAGS) -fo=$@ kpjoy.c + +$(TARGET): $(OBJ)/kpjoy.obj kpjoy.lnk | $(BIN) + $(LINK) option quiet @kpjoy.lnk name $@ file $(OBJ)/kpjoy.obj option map=$(OBJ)/kpjoy.map + +$(OBJ) $(BIN): + mkdir -p $@ + +clean: + rm -rf $(OBJ) $(BIN) + +.PHONY: all clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..b747566 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# KPJOY.DRV -- Game Port Joystick Driver for Windows 3.1 + +KPJOY.DRV is a joystick driver for Windows 3.1 and Windows for Workgroups +3.11. Windows 3.1 has the joystick API (`joyGetPos`, `joyGetDevCaps`, +`joySetCapture` and the rest, in MMSYSTEM) but ships no driver for it, so +without one no program ever sees a stick. KPJOY reads the PC game port's +two sticks (two axes and two buttons each, or one stick and a Y cable's +second) for every program that uses that API. + +It was written for JoeyLib's Win16 port, whose Windows runs it, and is +verified there under DOSBox Staging; it has not yet run on real hardware. + + +## Contents + +- [Files](#files) +- [Building](#building) +- [Installing](#installing) +- [How it works](#how-it-works) +- [What programs see](#what-programs-see) +- [Calibration](#calibration) +- [Limits](#limits) +- [Debug logging](#debug-logging) +- [Verification](#verification) +- [License](#license) + + +## Files + +| File | Contents | +|---|---| +| `kpjoy.c` | The driver: DriverProc, the game port reader, positions and calibration | +| `kpjoy.lnk` | wlink directives: a Windows DLL, module KPJOY, code and data fixed | +| `Makefile` | The build, for GNU make on Linux with Open Watcom v2 | +| `LICENSE.md` | The MIT License | + + +## Building + +Requires Open Watcom v2 with its 16-bit Windows libraries (`lib286/win`). +The Makefile uses the copy JoeyLib's installer sets up beside this project +(`../joeylib/toolchains/opt/openWatcom`) unless `WATCOM` names another. + +``` +make bin/kpjoy.drv +make LOG=1 the same, logging to C:\KPJOY.LOG +make clean removes obj/ and bin/ +make OBJ=DIR BIN=DIR builds somewhere else +``` + +Compiler flags (see `Makefile`): Windows target DLL (`-bt=windows -bd`), +small model with SS != DS (`-ms -zu`: a driver runs on its caller's stack), +the Windows prolog for exported functions (`-zw`), no stack probes (`-s`), +286 code (`-2`), 2-byte structure packing as the Windows headers expect +(`-zp2`), every warning, as an error. + +Two rules matter for anyone changing the code: + +- **No near pointers to locals.** With SS != DS a near pointer is + DS-relative, so a buffer handed to Windows (or reached through a pointer) + lives in DGROUP, not on the stack. +- **An `int` is 16 bits.** Arithmetic on positions (0..65535) and on the + counts is done in `uint32_t`; `32768u * 4` is 0 in an `unsigned int`. + +JoeyLib's installer builds it with this Makefile (its `kpjoy` component), +into its own build tree, so this folder is never written to. + + +## Installing + +1. Copy `kpjoy.drv` to `\WINDOWS\SYSTEM`. +2. In `SYSTEM.INI` `[drivers]`, set `joystick=kpjoy.drv`. +3. Restart Windows. + +MMSYSTEM loads and enables the driver itself (`DRV_LOAD`, `DRV_ENABLE`). +A game shipped for Windows 3.x can carry the driver and these two steps in +its own setup (see [License](#license)). + + +## How it works + +**The game port.** Writing I/O port 201h fires four one-shots, one per +axis (bits 0-3: stick A's X and Y, stick B's X and Y). Each holds its bit +at 1 for a time set by the axis's position: about 24 us at one end, about +0.7 ms in the middle, about 1.35 ms at the other. Bits 4-7 are the buttons +(stick A's 1 and 2, stick B's 1 and 2), 0 while pressed. An axis with +nothing plugged in never clears. + +**Timing.** The driver counts reads of the port until each axis's bit +clears. The count depends on the CPU's speed, so counts are only ever used +against each other (below). How long a missing axis is waited for, 4 ms, +is turned into a number of reads at enable by counting reads over one BIOS +tick (the BIOS data area's tick count, through a DPMI selector), so that +does not depend on the CPU either. + +**One reading for both sticks.** A reading times all four axes at once. +MMSYSTEM asks for each stick separately, so the reading is kept until both +sticks have had it: a program polling sticks A and B each frame costs one +busy wait per frame, not two. A stick that is not there is not waited for, +except every 64th reading, which looks for all four axes so a stick plugged +in since is found; a stick that is there and stops clearing is reported +unplugged. + +**MMSYSTEM's protocol.** MMSYSTEM opens the driver once per joystick: +`DRV_OPEN` with `lParam2` 0 for the first stick and 1 for the second, and +the driver's answer (the stick's index plus 1) comes back as the driver ID +of that stick's `JDD_*` messages. The messages are those of the Windows 3.1 +DDK's `mmddk.h`: + +| Message | Value | Answer | +|---|---|---| +| `JDD_GETNUMDEVS` | 0x0801 | 2 | +| `JDD_GETDEVCAPS` | 0x0802 | a JOYCAPS (lParam1), as much as lParam2's size holds | +| `JDD_GETPOS` | 0x0901 | a JOYINFO (lParam1), or `JOYERR_UNPLUGGED` | +| `JDD_SETCALIBRATION` | 0x0902 | sets the stick's calibration (below) | + +Everything else goes to `DefDriverProc`. The code and data segments are +fixed: `joySetCapture`'s timer calls in at interrupt time. + + +## What programs see + +- **JOYCAPS:** X and Y from 0 to 65535, no Z, two buttons, polling periods + 10 ms to 1000 ms; the name "KPJOY Game Port Joystick"; manufacturer and + product 0 (unregistered). +- **JOYINFO:** X and Y in 0..65535, the rest at 32768; buttons + `JOY_BUTTON1` and `JOY_BUTTON2`. +- **Unplugged:** `JDD_GETPOS` answers `JOYERR_UNPLUGGED`; MMSYSTEM passes it + on from `joyGetPos`. + + +## Calibration + +**Automatic (the default).** An axis's rest is taken from its first +readings, as the stick is centred then: leave it centred as the program +starts. Anything that takes the CPU away during a count -- an interrupt, +an emulator slowing down -- only ever shortens it, so over the first 64 +readings the rest rises to the longest reading that is not a push (one half +as long again as the rest is a push). The travel to either side is learned +as the stick moves: a full push is taken to be at least twice the rest's +count until the stick shows more. Readings within 4% of the rest report +exactly 32768, so a centred stick reads centred through jitter. +`DRV_ENABLE` starts it all again. + +**Explicit.** `JDD_SETCALIBRATION` (lParam1: the new values, lParam2, if +not NULL: where the old ones go, both in `mmddk.h`'s JOYCALIBRATE layout) +sets a stick's calibration: position = (count - base) * delta for X and Y, +kept to 0..65535, where count is the driver's raw count. From then on that +stick's readings use it as they are, with no dead band. + + +## Limits + +- Two sticks of two axes and two buttons. A four-button stick on one port + reads as stick A plus stick B's buttons; a throttle or rudder on the + second stick's axes reads as stick B. +- No Z axis and no configuration dialog (`DRV_QUERYCONFIGURE` answers 0). +- Interrupts stay on while the port is timed (a count of up to about 1.35 ms + with a stick in place), so a serial port at 115200 baud keeps its FIFO + served; the price is the jitter the dead band absorbs. +- Counts follow the CPU's speed. A real PC's is fixed, but an emulator that + varies its own (DOSBox at max cycles, which follows the host's load) + makes the positions drift with it, the rest included; give it a fixed + speed. + + +## Debug logging + +`make LOG=1` builds a driver that appends to `C:\KPJOY.LOG`: every message +but `JDD_GETPOS` (with its driver ID and parameters), the enable's +measurements, and a line for each change in what a stick reports, with the +counts and rests behind it. The log is written with KERNEL's file calls, +not at interrupt time; do not ship it. + + +## Verification + +JoeyLib's `scripts/verify-win16-joy.sh` runs JOY (JoeyLib's +`examples/joy`) on Windows for Workgroups 3.11 in DOSBox Staging, with +DOSBox's 4-axis game port driven from host keys, and reads both sticks' +whole state back off the screen: rest, each direction of each stick, two +diagonals each, each button alone and both together, a release after each, +a step holding lines on both sticks at once, and no stick at all when the +game port is disabled, with DOSBox's CPU at a fixed speed. + +Not yet done: a real 386 or 486 with an analog stick, a real Y cable, +`joySetCapture`, and a calibration tool's `JDD_SETCALIBRATION`. + + +## License + +KPJOY is distributed under the MIT License (`LICENSE.md`): it may be used, +changed and shipped, alone or with a program, as source or as `kpjoy.drv`, +as long as the copyright and permission notice go with it. + +The driver also carries Open Watcom code (its DLL start-up and C library +helpers), and Open Watcom's license (the Sybase Open Watcom Public +License, section 2.2(d)) asks for a notice, in the driver and in its +documentation, that the source of that code is available and where. +`kpjoy.drv` carries the notice (never shown); a program that ships the +driver puts this line in its documentation: + +``` +This software contains code from Open Watcom. Its source code is available +under the terms of the Sybase Open Watcom Public License version 1.0 from +https://github.com/open-watcom/open-watcom-v2 +``` diff --git a/kpjoy.c b/kpjoy.c new file mode 100644 index 0000000..018b6ad --- /dev/null +++ b/kpjoy.c @@ -0,0 +1,587 @@ +// kpjoy.c -- KPJOY.DRV, a joystick driver for Windows 3.1: the PC game +// port's two sticks for MMSYSTEM's joystick functions (joyGetPos, +// joyGetDevCaps and the rest). +// +// Windows 3.1 has the joystick API but no joystick driver of its own; +// MMSYSTEM loads the one SYSTEM.INI names ([drivers] joystick=kpjoy.drv) +// as an installable driver and talks to it through DriverProc. +// +// The game port (I/O port 201h): writing it fires four one-shots, one per +// axis (bits 0-3: stick A's X and Y, stick B's X and Y), each of which +// holds its bit at 1 for a time set by the axis's position, about 24 us at +// one end to about 1.35 ms at the other. Bits 4-7 are the buttons (stick +// A's 1 and 2, stick B's 1 and 2), 0 while pressed. An axis with nothing +// plugged in never clears. +// +// The driver times each axis by counting reads of the port until its bit +// clears. The counts depend on the CPU's speed, so they are only ever used +// against each other: an axis's rest is taken from its first readings, as +// the stick is centred then, and a position is reported in 0..65535 with +// the rest at 32768 (the stick's travel to either side is learned as it +// moves). Anything that takes the CPU away during a count (an interrupt, +// an emulator slowing down) only ever shortens it, so the rest settles on +// the longest of the first readings that are not a push; and readings +// within DEAD_BAND_PERCENT of the rest report exactly 32768, so a centred +// stick reads centred through that jitter. A program, or a calibration +// tool, can set explicit calibration instead (JDD_SETCALIBRATION), which +// is used as it is. How long a missing axis is waited for is measured +// against the BIOS tick at enable, so it does not depend on the CPU either. +// +// One read of the port times all four axes, and serves both sticks: a +// program asking for stick A and then stick B gets the same reading, so the +// port is busy-waited on once per pair. A stick that is not there is looked +// for again now and then, so plugging one in is noticed. +// +// The code and data are fixed: joySetCapture's timer calls in at interrupt +// time. + +#include +#include +#include +#include +#include + +// ----- Constants ----- + +// Logging to C:\KPJOY.LOG (build with -DKPJOY_LOG=1): every message but +// JDD_GETPOS, and each change in what a stick reports, with its counts. +#ifndef KPJOY_LOG +#define KPJOY_LOG 0 +#endif + +// The joystick driver messages (the Windows 3.1 DDK's mmddk.h). +#define JDD_GETNUMDEVS (DRV_RESERVED + 0x0001) +#define JDD_GETDEVCAPS (DRV_RESERVED + 0x0002) +#define JDD_GETPOS (DRV_RESERVED + 0x0101) +#define JDD_SETCALIBRATION (DRV_RESERVED + 0x0102) + +// The game port and its bits. +#define GAME_PORT 0x201 +#define AXIS_COUNT 4 +#define BUTTON_SHIFT 4 +#define BUTTON_MASK_ALL 0x0F +#define BUTTONS_PER_STICK 2 +#define BUTTON_STICK_MASK 0x03 + +// Two sticks of two axes each: stick A is axes 0 and 1, stick B 2 and 3. +#define STICK_COUNT 2 +#define AXES_PER_STICK 2 +#define STICK_AXIS_MASK 0x03 +#define AXIS_X 0 +#define AXIS_Y 1 + +// Positions as JOYCAPS reports their range. +#define POS_MIN 0u +#define POS_MAX 65535u +#define POS_REST 32768u + +// A full push to one side is taken to be at least twice the rest's count +// until the stick shows more. +#define TRAVEL_FACTOR 2u + +// The rest settles over an axis's first readings: one longer than the rest +// so far is the rest unless it is half as long again (a push). +#define REST_SETTLE_READINGS 64u +#define REST_PUSH_NUMERATOR 3u +#define REST_PUSH_DENOMINATOR 2u + +// Readings this close to the rest (percent of the travel to either end) +// report the rest itself. +#define DEAD_BAND_PERCENT 4u +// (In 32 bits: an int is 16 here, and POS_REST * 4 would overflow it.) +#define DEAD_BAND ((uint16_t)((uint32_t)POS_REST * DEAD_BAND_PERCENT / 100u)) + +// A missing axis is waited for this long; the longest a real one holds its +// bit is about 1.35 ms. +#define TIMEOUT_MS 4u + +// The BIOS tick: its count's place in the BIOS data area, and its period. +#define BIOS_DATA_SEGMENT 0x0040 +#define BIOS_TICK_OFFSET 0x006C +#define BIOS_TICK_MS 55u +// Reads of the port counted while waiting for a tick, at most: a tick that +// never comes must not hang the enable. +#define CALIBRATE_READS_MAX 50000000UL +// The fewest reads a timeout is made of, whatever the measurement says. +#define TIMEOUT_READS_MIN 1000UL + +// DPMI: a selector for a real-mode segment. +#define DPMI_INT 0x31 +#define DPMI_SEG_TO_DESC 0x0002 + +// A missing stick is looked for again after this many readings. +#define PROBE_READINGS 64u + +// What JOYCAPS says of the driver: no registered manufacturer or product, +// and the polling periods joySetCapture may ask for (ms). +#define KPJOY_MID 0 +#define KPJOY_PID 0 +#define KPJOY_NAME "KPJOY Game Port Joystick" +#define PERIOD_MIN_MS 10 +#define PERIOD_MAX_MS 1000 + +#if KPJOY_LOG +#define LOG_FILE "C:\\KPJOY.LOG" +#define LOG_LINE_MAX 160 +#define LOG_SEEK_END 2 +// A line: wsprintf into gLogLine (DGROUP: with SS != DS a buffer on the +// stack cannot be handed to Windows by a near pointer), then appended. +#define LOG(...) logWrite(wsprintf(gLogLine, __VA_ARGS__)) +#else +#define LOG(...) +#endif + +// ----- Types ----- + +// Calibration as JDD_SETCALIBRATION gives it (mmddk.h's JOYCALIBRATE): +// position = (count - base) * delta, for X, Y and Z. +typedef struct { + uint16_t wXbase; + uint16_t wXdelta; + uint16_t wYbase; + uint16_t wYdelta; + uint16_t wZbase; + uint16_t wZdelta; +} JoyCalibrateT; + +// What is known of one axis: its rest, the readings left for it to +// settle, and the longest reading yet. +typedef struct { + bool restKnown; + uint8_t settle; + uint32_t rest; + uint32_t high; +} AxisT; + +// One stick: whether it is there, whether it has explicit calibration and +// what, and whether it has yet to report the last reading. +typedef struct { + bool present; + bool calibrated; + bool fresh; + JoyCalibrateT calibration; +} StickT; + +// ----- Prototypes ----- + +static uint16_t axisPosition(uint8_t axis, uint32_t count); +static uint16_t biosSelector(void); +static uint16_t biosTick(void); +static uint16_t calibratedPosition(uint32_t count, uint16_t base, uint16_t delta); +static void driverEnable(void); +LRESULT FAR PASCAL _export DriverProc(DWORD driverId, HDRVR driver, UINT message, LPARAM param1, LPARAM param2); +static LRESULT getDevCaps(JOYCAPS FAR *caps, UINT size); +static LRESULT getPos(uint8_t stick, JOYINFO FAR *info); +int FAR PASCAL LibMain(HINSTANCE instance, WORD dataSeg, WORD heapSize, LPSTR cmdLine); +#if KPJOY_LOG +static void logWrite(int length); +#endif +static uint16_t lowPosition(uint32_t count, uint32_t rest); +static void readPort(void); +static LRESULT setCalibration(uint8_t stick, const JoyCalibrateT FAR *set, JoyCalibrateT FAR *previous); +static uint8_t stickOf(DWORD driverId); +int FAR PASCAL _export WEP(int param); + +// ----- Module state ----- + +static StickT gSticks[STICK_COUNT]; +static AxisT gAxes[AXIS_COUNT]; +// The last reading: each axis's count (gTimeoutReads if it never cleared) +// and the button bits (1 = pressed, stick A's in bits 0-1, B's in 2-3). +static uint32_t gCounts[AXIS_COUNT]; +static uint8_t gButtons; +static uint32_t gTimeoutReads = TIMEOUT_READS_MIN; +static uint16_t gReadings; +static uint16_t gBiosSelector; +// JDD_GETDEVCAPS's answer, in DGROUP (see LOG). +static JOYCAPS gCaps; +#if KPJOY_LOG +// What each stick last reported. +static UINT gLogX[STICK_COUNT]; +static UINT gLogY[STICK_COUNT]; +static UINT gLogButtons[STICK_COUNT]; +static char gLogLine[LOG_LINE_MAX]; +static OFSTRUCT gLogFile; +#endif + +// Open Watcom's license (the Sybase Open Watcom Public License, section +// 2.2(d)) asks for a notice, in the driver and in its documentation, that the +// source of the Open Watcom code it carries (its DLL start-up and C library +// helpers) is available, and where. This is the driver's copy, never shown; +// it is not static, so the compiler keeps it although nothing reads it. +// README.md gives the same text for the documentation. +const char owplNotice[] = "This software contains code from Open Watcom. Its source code is available under the terms of the Sybase Open Watcom Public License version 1.0 from https://github.com/open-watcom/open-watcom-v2"; + +// ----- Functions (alphabetical) ----- + +// An axis's count as a position: explicit calibration if its stick has +// one, else around the rest. +static uint16_t axisPosition(uint8_t axis, uint32_t count) { + AxisT *a; + StickT *s; + bool isY; + uint16_t travel; + uint16_t position; + + a = &gAxes[axis]; + s = &gSticks[axis / AXES_PER_STICK]; + isY = (axis % AXES_PER_STICK) == AXIS_Y; + if (s->calibrated) { + if (isY) { + return calibratedPosition(count, s->calibration.wYbase, s->calibration.wYdelta); + } + return calibratedPosition(count, s->calibration.wXbase, s->calibration.wXdelta); + } + if (!a->restKnown) { + a->rest = count; + a->high = count * TRAVEL_FACTOR; + a->settle = REST_SETTLE_READINGS; + a->restKnown = true; + } else if (a->settle > 0) { + a->settle--; + if (count > a->rest && count * REST_PUSH_DENOMINATOR < a->rest * REST_PUSH_NUMERATOR) { + a->rest = count; + if (a->high < count * TRAVEL_FACTOR) { + a->high = count * TRAVEL_FACTOR; + } + } + } + if (count > a->high) { + a->high = count; + } + if (count <= a->rest) { + position = lowPosition(count, a->rest); + } else if (a->high <= a->rest) { + position = (uint16_t)POS_MAX; + } else { + travel = lowPosition(count - a->rest, a->high - a->rest); + if (travel > POS_MAX - POS_REST) { + travel = (uint16_t)(POS_MAX - POS_REST); + } + position = (uint16_t)(POS_REST + travel); + } + if (position >= POS_REST - DEAD_BAND && position <= POS_REST + DEAD_BAND) { + return (uint16_t)POS_REST; + } + return position; +} + + +// A selector for the BIOS data area, from DPMI, made once. +static uint16_t biosSelector(void) { + uint16_t sel; + + if (gBiosSelector == 0) { + // 0 stays when DPMI refuses (carry set). + sel = 0; + _asm { + mov ax, DPMI_SEG_TO_DESC + mov bx, BIOS_DATA_SEGMENT + int DPMI_INT + jc refused + mov sel, ax + refused: + } + gBiosSelector = sel; + } + return gBiosSelector; +} + + +// The BIOS tick count's low word. +static uint16_t biosTick(void) { + uint16_t FAR *tick; + + tick = (uint16_t FAR *)MAKELP(biosSelector(), BIOS_TICK_OFFSET); + return *tick; +} + + +// position = (count - base) * delta, kept to 0..65535. +static uint16_t calibratedPosition(uint32_t count, uint16_t base, uint16_t delta) { + uint32_t position; + + if (count <= base) { + return (uint16_t)POS_MIN; + } + count -= base; + if (count > POS_MAX) { + return (uint16_t)POS_MAX; + } + position = count * delta; + if (position > POS_MAX) { + return (uint16_t)POS_MAX; + } + return (uint16_t)position; +} + + +// DRV_ENABLE: how many reads of the port make TIMEOUT_MS, counted over one +// whole BIOS tick (the first loop waits for a tick to begin), then a first +// look for both sticks. Rests and calibration start again. +static void driverEnable(void) { + uint16_t start; + uint32_t reads; + uint8_t i; + + reads = 0; + if (biosSelector() != 0) { + start = biosTick(); + while (biosTick() == start && reads < CALIBRATE_READS_MAX) { + reads++; + } + start = biosTick(); + reads = 0; + while (biosTick() == start && reads < CALIBRATE_READS_MAX) { + (void)inp(GAME_PORT); + reads++; + } + } + gTimeoutReads = reads / BIOS_TICK_MS * TIMEOUT_MS; + if (gTimeoutReads < TIMEOUT_READS_MIN) { + gTimeoutReads = TIMEOUT_READS_MIN; + } + for (i = 0; i < AXIS_COUNT; i++) { + gAxes[i].restKnown = false; + } + for (i = 0; i < STICK_COUNT; i++) { + gSticks[i].present = true; + gSticks[i].calibrated = false; + gSticks[i].fresh = false; + } + gReadings = 0; + readPort(); + LOG("enable: %lu reads per tick, timeout %lu reads; sticks %d %d", reads, gTimeoutReads, (int)gSticks[0].present, (int)gSticks[1].present); +} + + +LRESULT FAR PASCAL _export DriverProc(DWORD driverId, HDRVR driver, UINT message, LPARAM param1, LPARAM param2) { + if (message != JDD_GETPOS) { + LOG("message %04x id %lx param1 %lx param2 %lx", message, driverId, param1, param2); + } + switch (message) { + case DRV_LOAD: + case DRV_FREE: + case DRV_CLOSE: + case DRV_DISABLE: + return 1L; + case DRV_ENABLE: + driverEnable(); + return 1L; + case DRV_OPEN: + // MMSYSTEM opens an instance for each stick, lParam2 0 and 1; the + // answer is the ID the stick's messages come with (stickOf). + if (param2 >= 0 && param2 < STICK_COUNT) { + return (LRESULT)(param2 + 1); + } + return 1L; + case DRV_QUERYCONFIGURE: + return 0L; + case DRV_INSTALL: + case DRV_REMOVE: + return (LRESULT)DRVCNF_OK; + case JDD_GETNUMDEVS: + return (LRESULT)STICK_COUNT; + case JDD_GETDEVCAPS: + return getDevCaps((JOYCAPS FAR *)param1, (UINT)param2); + case JDD_GETPOS: + return getPos(stickOf(driverId), (JOYINFO FAR *)param1); + case JDD_SETCALIBRATION: + return setCalibration(stickOf(driverId), (const JoyCalibrateT FAR *)param1, (JoyCalibrateT FAR *)param2); + default: + return DefDriverProc(driverId, driver, message, param1, param2); + } +} + + +// JDD_GETDEVCAPS: as much of JOYCAPS as the caller's size holds. +static LRESULT getDevCaps(JOYCAPS FAR *caps, UINT size) { + const char *from; + char *to; + UINT i; + + if (caps == NULL) { + return (LRESULT)JOYERR_PARMS; + } + gCaps.wMid = KPJOY_MID; + gCaps.wPid = KPJOY_PID; + gCaps.wXmin = POS_MIN; + gCaps.wXmax = POS_MAX; + gCaps.wYmin = POS_MIN; + gCaps.wYmax = POS_MAX; + gCaps.wZmin = POS_MIN; + gCaps.wZmax = POS_MIN; + gCaps.wNumButtons = BUTTONS_PER_STICK; + gCaps.wPeriodMin = PERIOD_MIN_MS; + gCaps.wPeriodMax = PERIOD_MAX_MS; + from = KPJOY_NAME; + to = gCaps.szPname; + while (*from != '\0' && to < gCaps.szPname + MAXPNAMELEN - 1) { + *to++ = *from++; + } + *to = '\0'; + for (i = 0; i < size && i < sizeof(gCaps); i++) { + ((char FAR *)caps)[i] = ((char *)&gCaps)[i]; + } + return (LRESULT)JOYERR_NOERROR; +} + + +// JDD_GETPOS: a new reading of the port unless this stick has not had the +// last one yet. +static LRESULT getPos(uint8_t stick, JOYINFO FAR *info) { + StickT *s; + uint8_t first; + + if (info == NULL) { + return (LRESULT)JOYERR_PARMS; + } + s = &gSticks[stick]; + if (!s->fresh) { + readPort(); + } + s->fresh = false; + if (!s->present) { + return (LRESULT)JOYERR_UNPLUGGED; + } + first = (uint8_t)(stick * AXES_PER_STICK); + info->wXpos = axisPosition((uint8_t)(first + AXIS_X), gCounts[first + AXIS_X]); + info->wYpos = axisPosition((uint8_t)(first + AXIS_Y), gCounts[first + AXIS_Y]); + info->wZpos = 0; + info->wButtons = (gButtons >> (stick * BUTTONS_PER_STICK)) & BUTTON_STICK_MASK; +#if KPJOY_LOG + if (info->wXpos != gLogX[stick] || info->wYpos != gLogY[stick] || info->wButtons != gLogButtons[stick]) { + LOG("reading %u: stick %d at %u %u buttons %u, counts %lu %lu, rests %lu %lu", gReadings, (int)stick, info->wXpos, info->wYpos, info->wButtons, gCounts[first + AXIS_X], gCounts[first + AXIS_Y], gAxes[first + AXIS_X].rest, gAxes[first + AXIS_Y].rest); + gLogX[stick] = info->wXpos; + gLogY[stick] = info->wYpos; + gLogButtons[stick] = info->wButtons; + } +#endif + return (LRESULT)JOYERR_NOERROR; +} + + +int FAR PASCAL LibMain(HINSTANCE instance, WORD dataSeg, WORD heapSize, LPSTR cmdLine) { + (void)instance; + (void)dataSeg; + (void)heapSize; + (void)cmdLine; + return 1; +} + + +#if KPJOY_LOG +// Append gLogLine's length characters, and a line end, to LOG_FILE. +static void logWrite(int length) { + HFILE file; + + gLogLine[length++] = '\r'; + gLogLine[length++] = '\n'; + file = OpenFile(LOG_FILE, &gLogFile, OF_WRITE); + if (file == HFILE_ERROR) { + file = OpenFile(LOG_FILE, &gLogFile, OF_CREATE | OF_WRITE); + } + if (file == HFILE_ERROR) { + return; + } + _llseek(file, 0L, LOG_SEEK_END); + _lwrite(file, gLogLine, (UINT)length); + _lclose(file); +} +#endif + + +// count / rest as 0..32768: both are cut down first so the product fits. +static uint16_t lowPosition(uint32_t count, uint32_t rest) { + while (rest > 0xFFFFUL) { + rest >>= 1; + count >>= 1; + } + if (rest == 0 || count >= rest) { + return (uint16_t)POS_REST; + } + return (uint16_t)(count * POS_REST / rest); +} + + +// One reading: fire the one-shots and count reads of the port until each +// awaited axis clears. The axes of the sticks that are there are awaited; +// now and then all four are, so a stick plugged in since is found. A stick +// that is there and does not clear is gone. +static void readPort(void) { + uint32_t reads; + uint8_t pending; + uint8_t cleared; + uint8_t value; + uint8_t axis; + uint8_t i; + bool probe; + + probe = gReadings % PROBE_READINGS == 0; + gReadings++; + pending = 0; + for (i = 0; i < STICK_COUNT; i++) { + if (probe || gSticks[i].present) { + pending |= (uint8_t)(STICK_AXIS_MASK << (i * AXES_PER_STICK)); + } + } + for (axis = 0; axis < AXIS_COUNT; axis++) { + gCounts[axis] = gTimeoutReads; + } + outp(GAME_PORT, 0); + reads = 0; + while (pending != 0 && reads < gTimeoutReads) { + value = (uint8_t)inp(GAME_PORT); + cleared = (uint8_t)(pending & ~value); + if (cleared != 0) { + for (axis = 0; axis < AXIS_COUNT; axis++) { + if ((cleared & (1u << axis)) != 0) { + gCounts[axis] = reads; + } + } + pending &= (uint8_t)~cleared; + } + reads++; + } + value = (uint8_t)inp(GAME_PORT); + gButtons = (uint8_t)((~value >> BUTTON_SHIFT) & BUTTON_MASK_ALL); + for (i = 0; i < STICK_COUNT; i++) { + if (probe || gSticks[i].present) { + gSticks[i].present = gCounts[i * AXES_PER_STICK + AXIS_X] < gTimeoutReads && gCounts[i * AXES_PER_STICK + AXIS_Y] < gTimeoutReads; + } + gSticks[i].fresh = true; + } +} + + +// JDD_SETCALIBRATION: the stick's calibration from now on; the one it had +// goes to previous. +static LRESULT setCalibration(uint8_t stick, const JoyCalibrateT FAR *set, JoyCalibrateT FAR *previous) { + StickT *s; + + s = &gSticks[stick]; + if (previous != NULL) { + *previous = s->calibration; + } + if (set == NULL) { + return (LRESULT)JOYERR_PARMS; + } + s->calibration = *set; + s->calibrated = true; + return (LRESULT)JOYERR_NOERROR; +} + + +// The stick a driver instance is for: DRV_OPEN returned its index plus 1. +static uint8_t stickOf(DWORD driverId) { + if (driverId >= 1 && driverId <= STICK_COUNT) { + return (uint8_t)(driverId - 1); + } + return 0; +} + + +int FAR PASCAL _export WEP(int param) { + (void)param; + return 1; +} diff --git a/kpjoy.lnk b/kpjoy.lnk new file mode 100644 index 0000000..b0306bc --- /dev/null +++ b/kpjoy.lnk @@ -0,0 +1,11 @@ +# kpjoy.lnk -- wlink directives for KPJOY.DRV: a Windows 3.x installable +# driver (a DLL whose entry point MMSYSTEM looks up is DriverProc), its code +# and data fixed in memory, as joySetCapture's timer calls in at interrupt +# time. DriverProc and WEP are exported by the source (_export). The +# Makefile adds the output name and the object. +system windows_dll +option modname=KPJOY +option description 'KPJOY Game Port Joystick Driver' +option heapsize=1024 +segment type code preload fixed +segment type data preload fixed