Initial commit.

This commit is contained in:
Scott Duensing 2026-10-13 13:54:56 -05:00
commit 75ff3849bf
6 changed files with 871 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Build outputs (make)
obj/
bin/

21
LICENSE.md Normal file
View file

@ -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.

42
Makefile Normal file
View file

@ -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

207
README.md Normal file
View file

@ -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
```

587
kpjoy.c Normal file
View file

@ -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 <windows.h>
#include <mmsystem.h>
#include <conio.h>
#include <stdbool.h>
#include <stdint.h>
// ----- 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;
}

11
kpjoy.lnk Normal file
View file

@ -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