Initial commit.
This commit is contained in:
commit
75ff3849bf
6 changed files with 871 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Build outputs (make)
|
||||
obj/
|
||||
bin/
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal 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
42
Makefile
Normal 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
207
README.md
Normal 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
587
kpjoy.c
Normal 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
11
kpjoy.lnk
Normal 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
|
||||
Loading…
Add table
Reference in a new issue