More DVX target work.

This commit is contained in:
Scott Duensing 2026-09-29 21:25:37 -05:00
parent 17343a0b4f
commit c520b16179
29 changed files with 1269 additions and 366 deletions

1
.gitignore vendored
View file

@ -63,6 +63,7 @@ nvram/
# Crap I added # Crap I added
stuff/* stuff/*
__pycache__/ __pycache__/
screenshots/
# Claude's working notes (design scratch, port write-ups, session state). # Claude's working notes (design scratch, port write-ups, session state).
# NOT the user manual -- that lives in docs/ and IS tracked. # NOT the user manual -- that lives in docs/ and IS tracked.

View file

@ -8,8 +8,9 @@ platforms, and one DOS desktop, from a single codebase:
- Atari ST (STF / 68000 baseline) - Atari ST (STF / 68000 baseline)
- MS-DOS (386 / VGA, DJGPP) - MS-DOS (386 / VGA, DJGPP)
- Sharp X68000 (68000 @ 10 MHz, Human68k, elf2x68k) - Sharp X68000 (68000 @ 10 MHz, Human68k, elf2x68k)
- DVX (the DOS desktop GUI in `../dvxgui`: each program a windowed `.app`, - DVX (the DOS desktop GUI in `../dvxgui`: each program a windowed `.app`
built with the DOS compiler against DVX's SDK) that can also take the whole screen, built with the DOS compiler against
DVX's SDK)
The Apple IIgs defines the capability ceiling. Stronger platforms coast. The Apple IIgs defines the capability ceiling. Stronger platforms coast.
Hot paths are hand-written assembly per port; the public API is C. Hot paths are hand-written assembly per port; the public API is C.
@ -49,10 +50,10 @@ the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`,
`adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception: `adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception:
`make iigs` builds the library, and `make iigs-disk` builds its examples and `make iigs` builds the library, and `make iigs-disk` builds its examples and
packages them onto a disk image. DVX needs the `dvx` installer component, packages them onto a disk image. DVX needs the `dvx` installer component,
which copies DVX and its SDK from a built DVX tree which unpacks DVX and its SDK from a DVX tree's distribution, `dist/dvx.zip`
(`./install.sh --dvx-dir PATH install dvx`; the default is `../dvxgui`); its and `dist/sdk.zip` (`./install.sh --dvx-dir PATH install dvx`; the default
examples land in `build/dvx/bin/apps/joeylib/<name>/`, laid out as they is `../dvxgui`); its examples land in `build/dvx/bin/apps/joeylib/<name>/`,
install in DVX. laid out as they install in DVX.
Space Taxi's levels come from the original Commodore 64 game, which is not in Space Taxi's levels come from the original Commodore 64 game, which is not in
the repository. Without `stuff/spacetaxi/raw.bin` the build compiles the the repository. Without `stuff/spacetaxi/raw.bin` the build compiles the

View file

@ -194,8 +194,9 @@ uint16_t jlFrameHz(void);
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs, *Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs,
ST NTSC and X68000, 70 on VGA mode 13h and on DVX, whose window has a virtual ST NTSC and X68000, 70 on VGA mode 13h and on DVX, whose window has a virtual
70 Hz. The real cadence may drift slightly; this is the figure to divide by 70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the same
when converting frames to seconds. frame count). The real cadence may drift slightly; this is the figure to
divide by when converting frames to seconds.
=== jlMillisElapsed === jlMillisElapsed

View file

@ -141,7 +141,8 @@ bool jlMousePresent(void);
the INT 33h driver reset successfully; on Amiga, ST, IIgs, X68000 and DVX the the INT 33h driver reset successfully; on Amiga, ST, IIgs, X68000 and DVX the
OS mouse is always wired. On DVX the position is the pointer's over the OS mouse is always wired. On DVX the position is the pointer's over the
program's window, divided by the window's scale; it keeps its last value program's window, divided by the window's scale; it keeps its last value
while the pointer is elsewhere. while the pointer is elsewhere. On DVX's full screen it is the pointer's
place on the stage (DVX draws no pointer there).
=== jlMouseX / jlMouseY === jlMouseX / jlMouseY

View file

@ -200,9 +200,7 @@ Closing does *not* hang up: `jlSerialClose` leaves DTR exactly as it is, so
a modem call or a BBS caller survives the program handing the line back: a a modem call or a BBS caller survives the program handing the line back: a
door returning to its BBS, or a terminal program exiting while the call stays door returning to its BBS, or a terminal program exiting while the call stays
up. RTS is not part of that promise; on the Amiga, `serial.device` puts back up. RTS is not part of that promise; on the Amiga, `serial.device` puts back
whatever RTS was before the open. DVX is the exception: its serial library whatever RTS was before the open.
resets the modem lines when it closes a port, so there closing *does* drop
DTR, and a program that must hand a live call on has to keep the port open.
To hang up, drop DTR first: call `jlSerialSetDtr(false)`, wait longer than To hang up, drop DTR first: call `jlSerialSetDtr(false)`, wait longer than
the modem needs to notice (a Hayes modem's S25 setting, 50 ms by default), the modem needs to notice (a Hayes modem's S25 setting, 50 ms by default),

View file

@ -123,7 +123,7 @@ firmware.
| `all` | `all`
| Everything except optional components (`staxiData`, Space Taxi's level | Everything except optional components (`staxiData`, Space Taxi's level
source, and `dvx`, which copies DVX from a DVX tree of yours), which are source, and `dvx`, which unpacks DVX from a DVX tree of yours), which are
installed by name. `toolchains` and `emulators` leave them out too. installed by name. `toolchains` and `emulators` leave them out too.
|=== |===
@ -152,11 +152,13 @@ it. For working on the compiler and JoeyLib together. The setting is
remembered; `--llvm816-dir clone` goes back to a managed clone. remembered; `--llvm816-dir clone` goes back to a managed clone.
| `--dvx-dir PATH` | `--dvx-dir PATH`
| The built DVX tree the `dvx` component copies DVX and its SDK from. The | The DVX tree the `dvx` component takes DVX and its SDK from: its
default is a `dvxgui` checkout beside this repository; the setting is distribution, `dist/dvx.zip` (DVX as it installs) and `dist/sdk.zip` (the
remembered. The component only reads that tree: it copies what the builds SDK). The default is a `dvxgui` checkout beside this repository; the
and the emulator runs need into `toolchains/opt/dvx`, and rebuilding DVX setting is remembered. The component only reads that tree: it unpacks both
there makes the component out of date, so `install dvx` copies it again. zips into `toolchains/opt/dvx`, and a new zip there makes the component out
of date, so `install dvx` unpacks it again. (A tree whose `dist/` has no
`sdk.zip` yet gives its built SDK, `bin/sdk`, instead.)
|=== |===
Everything the installer creates stays inside `toolchains/`: Everything the installer creates stays inside `toolchains/`:

View file

@ -39,8 +39,9 @@ them:
|DVX |DVX
|486 or later |486 or later
|what DVX needs |what DVX needs
|A program in a window on the DVX desktop, at 1x to 3x. The MS-DOS port's |A program in a window on the DVX desktop, at 1x to 3x, or on the whole
stage and code; a virtual 70 Hz. See <<_dvx_programs>>. screen at 320x200. The MS-DOS port's stage and code; a virtual 70 Hz. See
<<_dvx_programs>>.
|=== |===
The Apple IIgs is deliberately the slowest target and the design reference. If The Apple IIgs is deliberately the slowest target and the design reference. If

View file

@ -209,7 +209,7 @@ The API is uniform; these are the places where what you get differs.
|60 Hz (NTSC) |60 Hz (NTSC)
|70 Hz |70 Hz
|60 Hz |60 Hz
|70 Hz (virtual) |70 Hz (virtual; the real retrace on the whole screen)
|Pixel storage |Pixel storage
|chunky 4bpp |chunky 4bpp
@ -295,20 +295,38 @@ running in a window of its own beside the others. The graphics are the
MS-DOS port's -- the same chunky stage and the same x86 code, so UBER's MS-DOS port's -- the same chunky stage and the same x86 code, so UBER's
hashes are DOS's -- and the rest behaves like this: hashes are DOS's -- and the rest behaves like this:
* *A window, not the screen.* The window's content is the 320x200 stage at * *A window, or the whole screen.* The window's content is the 320x200 stage
the largest whole scale, up to 3x, that fits the desktop: 1x at 640x480, at the largest whole scale, up to 3x, that fits the desktop: 1x at
2x at 800x600, 3x at 1024x768. The stage reaches the desktop through one 640x480, 2x at 800x600, 3x at 1024x768. The stage reaches the desktop
colour table per palette, so SCB palettes are exact on 15-, 16- and 32-bit through one colour table per palette, so SCB palettes are exact on 15-,
desktops. On a 256-colour desktop every colour is the nearest one in DVX's 16- and 32-bit desktops. On a 256-colour desktop every colour is the
palette, which all programs share: gradients band. nearest one in DVX's palette, which all programs share: gradients band.
* *Full screen.* Game > Full Screen, or Alt+Enter, puts the program on the
whole screen in DVX's full-screen mode, 320x200 in 256 colours (VGA mode
13h, or a VBE mode of that size), and back in its window. There it looks
exactly as on MS-DOS: the screen's 256 colours hold all 16 palettes, so
SCB palettes are exact at any desktop depth and palette changes are free.
The other programs run on meanwhile. Alt+Tab, Ctrl+Esc or another window
coming to the front brings the desktop back, with the program still
running and drawing in its window; giving its window the focus again puts
it back on the whole screen. The choice is kept in the program's settings
folder (`JOEYLIB.INI`), so it starts the way it was left. It is the user's
choice: JoeyLib's API has no full-screen call. (Under DOSBox, Alt+Enter is
DOSBox's own full-screen key; use Alt with the keypad's Enter, or the
menu.)
* *A virtual 70 Hz.* A window has no vertical blank. `jlFrameHz()` is 70, as * *A virtual 70 Hz.* A window has no vertical blank. `jlFrameHz()` is 70, as
on DOS, and `jlWaitVBL()` waits for the next seventieth of a second of on DOS, and `jlWaitVBL()` waits for the next seventieth of a second of
DVX's clock while the other programs run. Nothing prevents tearing. DVX's clock while the other programs run; nothing prevents tearing. On the
* *Keys DVX keeps.* While the window is active every key goes to the program, whole screen `jlWaitVBL()` gives the other programs a turn, then waits for
F1, F10 and Alt included, except Alt+Tab (switch programs), Ctrl+Esc (Task the screen's real vertical retrace, as on DOS, so a present right after it
Manager) and Alt+F4 (close the window). The window's Game menu has Help does not tear. The frame count is the same clock in both, so switching
(the program's help file) and Exit. Typed characters come through DVX's never makes it jump.
keyboard layout. * *Keys DVX keeps.* While the window is active, or the program has the whole
screen, every key goes to the program, F1, F10 and Alt included, except
Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close the
window), and Alt+Enter, which the port keeps for full screen. The window's
Game menu has Full Screen, Help (the program's help file) and Exit. Typed
characters come through DVX's keyboard layout.
* *Closing ends the program.* Alt+F4, the close box and Game > Exit end the * *Closing ends the program.* Alt+F4, the close box and Game > Exit end the
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent` or program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent` or
`jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then `jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then
@ -330,11 +348,11 @@ hashes are DOS's -- and the rest behaves like this:
be called from an interrupt. be called from an interrupt.
* *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes. * *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes.
DVX's keyboard joystick is not reported as one. DVX's keyboard joystick is not reported as one.
* *Serial ports* belong to DVX's serial library, one program each. Closing a * *Serial ports* belong to DVX's serial library, one program each. As on
port there drops DTR, so a modem hangs up. every port, closing one leaves DTR as it is.
* A program's `main` is compiled as `jlGameMain`, which the port calls with * A program's `main` is compiled as `jlGameMain`, which the port calls with
`argv[0]` the `.app`'s path. `argv[0]` the `.app`'s path.
`make dvx` builds every example as a DVX program (the `dvx` installer `make dvx` builds every example as a DVX program (the `dvx` installer
component supplies the SDK, <<_setting_up_the_toolchains>>), and component supplies DVX and its SDK, <<_setting_up_the_toolchains>>), and
`./joey-run --port dvx NAME` starts one in DVX under DOSBox Staging. `./joey-run --port dvx NAME` starts one in DVX under DOSBox Staging.

View file

@ -2,8 +2,65 @@
// runs even where the graphics HAL can't (e.g. AROS on the Amiga). Opens the // runs even where the graphics HAL can't (e.g. AROS on the Amiga). Opens the
// DEFAULT port at 9600 8N1, sends a banner (TX heartbeat), then echoes every // DEFAULT port at 9600 8N1, sends a banner (TX heartbeat), then echoes every
// received byte forever. A host serial peer verifies banner (TX) + echo (RX). // received byte forever. A host serial peer verifies banner (TX) + echo (RX).
//
// A peer that sees the modem lines (DOSBox's nullmodem reports them in the
// byte stream as they change) can also send DTR_CHECK: the program then
// drops and raises DTR, closes and reopens the port, drops DTR and closes
// the port for good, with a marker line after each step, so the peer can
// check that jlSerialSetDtr reaches the line and that jlSerialClose leaves
// DTR as it is, up or down. After the check the port stays closed.
#include <joey/serial.h> #include <joey/serial.h>
#define DTR_CHECK 0x04u // ASCII EOT: run the DTR check (not echoed)
#define DTR_ACK 0x06u // ASCII ACK: the peer has the whole marker line
static void dtrCheck(const jlSerialConfigT *cfg);
static void sendMark(const char *text);
static void dtrCheck(const jlSerialConfigT *cfg) {
jlSerialSetDtr(false);
sendMark("JLDTR-OFF\r\n");
jlSerialSetDtr(true);
sendMark("JLDTR-ON\r\n");
jlSerialClose();
if (!jlSerialOpen(JL_SERIAL_DEFAULT, cfg)) {
return;
}
sendMark("JLDTR-REOPENED\r\n");
jlSerialSetDtr(false);
sendMark("JLDTR-HANGUP\r\n");
jlSerialClose();
}
// A marker line, then the peer's DTR_ACK: the line has gone out whole, so
// the next line change comes after it (a changed line is reported at once,
// while bytes wait their turn in the UART), and a close cannot throw any of
// it away.
static void sendMark(const char *text) {
uint16_t len;
uint16_t sent;
uint8_t byte;
len = 0u;
while (text[len] != '\0') {
len++;
}
sent = 0u;
while (sent < len) {
jlSerialPoll();
sent = (uint16_t)(sent + jlSerialWrite((const uint8_t *)&text[sent], (uint16_t)(len - sent)));
}
for (;;) {
jlSerialPoll();
if (jlSerialRead(&byte, 1u) == 1u && byte == DTR_ACK) {
return;
}
}
}
int main(void) { int main(void) {
jlSerialConfigT cfg; jlSerialConfigT cfg;
static const char banner[] = "JLSERTEST-READY\r\n"; static const char banner[] = "JLSERTEST-READY\r\n";
@ -22,12 +79,21 @@ int main(void) {
for (;;) { for (;;) {
uint8_t buf[64]; uint8_t buf[64];
uint16_t n; uint16_t n;
uint16_t i;
jlSerialPoll(); jlSerialPoll();
n = jlSerialRead(buf, (uint16_t)sizeof(buf)); n = jlSerialRead(buf, (uint16_t)sizeof(buf));
if (n > 0u) { for (i = 0u; i < n; i++) {
jlSerialWrite(buf, n); if (buf[i] == DTR_CHECK) {
break;
} }
} }
/* not reached */ if (i > 0u) {
jlSerialWrite(buf, i);
}
if (i < n) {
dtrCheck(&cfg);
}
}
// not reached
} }

View file

@ -98,7 +98,8 @@
#define JOEYLIB_PLATFORM_NAME "Sharp X68000" #define JOEYLIB_PLATFORM_NAME "Sharp X68000"
#elif defined(JOEYLIB_PLATFORM_DVX) #elif defined(JOEYLIB_PLATFORM_DVX)
// The DOS stage (chunky 4bpp, x86 sprite JIT and tile walker from // The DOS stage (chunky 4bpp, x86 sprite JIT and tile walker from
// src/x86/), shown in a DVX window instead of on the VGA. // src/x86/), shown in a DVX window, or on DVX's full screen, instead of
// on the VGA.
#define JOEYLIB_CPU_X86 #define JOEYLIB_CPU_X86
#define JOEYLIB_ENDIAN_LITTLE #define JOEYLIB_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY #define JOEYLIB_NATIVE_CHUNKY
@ -470,13 +471,14 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#elif defined(JOEYLIB_PLATFORM_DVX) #elif defined(JOEYLIB_PLATFORM_DVX)
// A window on the DVX desktop (src/dvx). The graphics are the DOS port's: // A window on the DVX desktop, or DVX's full screen (src/dvx). The
// graphics are the DOS port's:
// the chunky generics, the x86 whole-map tile walker (src/x86/x86Tile.h, // the chunky generics, the x86 whole-map tile walker (src/x86/x86Tile.h,
// through the jlpTileMapPaste alias) and the x86 sprite JIT. DVX's // through the jlpTileMapPaste alias) and the x86 sprite JIT. DVX's
// libraries supply the services. // libraries supply the services.
#define JL_HAS_INIT // the program's window #define JL_HAS_INIT // the program's window
#define JL_HAS_SHUTDOWN #define JL_HAS_SHUTDOWN
#define JL_HAS_PRESENT // dirty rows through per-palette tables into the window #define JL_HAS_PRESENT // dirty rows through per-palette tables into the window or onto the full screen
#define JL_HAS_INPUT_INIT #define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN #define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL // held-key table, onKey characters, onMouse, joystick.lib #define JL_HAS_INPUT_POLL // held-key table, onKey characters, onMouse, joystick.lib

View file

@ -25,7 +25,7 @@
# toolchain:PORT emulator:PORT PORT toolchains emulators all # toolchain:PORT emulator:PORT PORT toolchains emulators all
# where PORT is iigs, amiga, atarist, dos, x68000 or dvx. `all`, `toolchains` # where PORT is iigs, amiga, atarist, dos, x68000 or dvx. `all`, `toolchains`
# and `emulators` leave out optional components (staxiData, and dvx, which # and `emulators` leave out optional components (staxiData, and dvx, which
# copies from a DVX tree you have); name those explicitly. # unpacks a DVX tree's dist/ zips you have); name those explicitly.
# #
# Options: # Options:
# -y, --yes install missing host packages without asking # -y, --yes install missing host packages without asking
@ -35,8 +35,9 @@
# --keep-build keep the scratch build trees in cache/build # --keep-build keep the scratch build trees in cache/build
# --llvm816-dir PATH use an existing llvm816 checkout for the IIgs # --llvm816-dir PATH use an existing llvm816 checkout for the IIgs
# compiler (remembered; "--llvm816-dir clone" undoes it) # compiler (remembered; "--llvm816-dir clone" undoes it)
# --dvx-dir PATH the built DVX tree the dvx component copies from # --dvx-dir PATH the DVX tree the dvx component unpacks DVX and its SDK
# (remembered; default: dvxgui beside this repository) # from (its dist/dvx.zip and dist/sdk.zip; remembered;
# default: dvxgui beside this repository)
# #
# Examples: # Examples:
# ./install.sh install toolchains every cross compiler # ./install.sh install toolchains every cross compiler
@ -422,7 +423,7 @@ while [ $# -gt 0 ]; do
;; ;;
--dvx-dir) --dvx-dir)
[ $# -ge 2 ] || die "--dvx-dir needs a path" [ $# -ge 2 ] || die "--dvx-dir needs a path"
[ -f "$2/bin/dvx.exe" ] || die "$2 does not look like a built DVX tree (no bin/dvx.exe)" [ -f "$2/dist/dvx.zip" ] || die "$2 does not look like a DVX tree with its distribution made (no dist/dvx.zip)"
mkdir -p "$JOEY_STATE" mkdir -p "$JOEY_STATE"
(cd "$2" && pwd) > "$JOEY_STATE/dvxDir" (cd "$2" && pwd) > "$JOEY_STATE/dvxDir"
shift shift

View file

@ -1,56 +1,68 @@
# DVX, the DOS desktop JoeyLib's dvx port runs in: its SDK (the headers, # DVX, the DOS desktop JoeyLib's dvx port runs in: the runtime the gates and
# sdk.mk and the host tools every DVX program is built with) and the runtime # joey-run boot (dvx.exe with its libraries, widgets, programs, settings and
# tree the gates and joey-run boot (dvx.exe with its libraries, widgets, # system files) and its SDK (the headers, sdk.mk and the host tools every DVX
# programs, settings and system files). Copied from a built DVX tree, so the # program is built with). Both are unpacked from a DVX tree's distribution:
# builds and the emulator runs never touch the DVX project itself. # dist/dvx.zip is the DVX installation and dist/sdk.zip the SDK (all of it
# under DVX-SDK/). A tree whose dist/ has no sdk.zip gives its built SDK,
# bin/sdk, instead. The builds and the emulator runs use the copy in
# toolchains/opt/dvx and never touch the DVX project itself.
# #
# `install.sh --dvx-dir PATH` names the DVX tree (remembered); the default is # `install.sh --dvx-dir PATH` names the DVX tree (remembered); the default is
# a dvxgui checkout beside this repository. Rebuilding DVX there makes this # a dvxgui checkout beside this repository. The pin is the zips' checksums, so
# component out of date, and `install dvx` copies it again. # a new dvx.zip or sdk.zip there makes this component out of date, and
# `install dvx` unpacks it again.
compDescription="DVX: desktop runtime + SDK, copied from a DVX tree (--dvx-dir)" compDescription="DVX: desktop runtime + SDK, unpacked from a DVX tree's dist/ zips (--dvx-dir)"
# Optional: it needs a DVX tree of your own, so `all` and `toolchains` leave # Optional: it needs a DVX tree of your own, so `all` and `toolchains` leave
# it out; `install dvx` (or toolchain:dvx) asks for it. # it out; `install dvx` (or toolchain:dvx) asks for it.
compGroups="toolchain:dvx optional" compGroups="toolchain:dvx optional"
compRequires="host djgpp" compRequires="host djgpp"
compHostDeps="" compHostDeps="unzip"
dvxDir="$(cat "$JOEY_STATE/dvxDir" 2>/dev/null || true)" dvxDir="$(cat "$JOEY_STATE/dvxDir" 2>/dev/null || true)"
if [ -z "$dvxDir" ]; then if [ -z "$dvxDir" ]; then
dvxDir="$(cd "$JOEY_REPO/.." && pwd)/dvxgui" dvxDir="$(cd "$JOEY_REPO/.." && pwd)/dvxgui"
fi fi
dvxBin="$dvxDir/bin" dvxRuntimeZip="$dvxDir/dist/dvx.zip"
# What the runtime needs from bin/: not the host test tools (bin/host), dvxSdkZip="$dvxDir/dist/sdk.zip"
# the test modules, the link map, the log or the installer. # The folder sdk.zip holds the SDK in.
dvxRuntime="dvx.exe config system libs widgets apps" dvxSdkZipRoot="DVX-SDK"
# The SDK as DVX's build leaves it: used only when dist/ has no sdk.zip.
dvxSdkTree="$dvxDir/bin/sdk"
# Where the SDK goes in the install.
dvxSdk="sdk" dvxSdk="sdk"
# dvxListing -- every file copied, with its size and time: the pin, so a # dvxSdkPin -- what the SDK comes from: sdk.zip's checksum, or without it
# rebuilt DVX makes this component out of date. # every file of bin/sdk with its size and time.
dvxListing() { dvxSdkPin() {
local part if [ -f "$dvxSdkZip" ]; then
[ -d "$dvxBin" ] || return 0 printf 'zip:%s\n' "$(sha256Of "$dvxSdkZip")"
for part in $dvxRuntime $dvxSdk; do elif [ -d "$dvxSdkTree" ]; then
[ -e "$dvxBin/$part" ] || continue printf 'tree:%s\n' "$(find "$dvxSdkTree" -type f -printf '%P %s %T@\n' | sort | sha1sum | cut -d' ' -f1)"
find "$dvxBin/$part" -type f -printf '%P %s %T@\n' fi
done | sort
} }
compPin="dir:$dvxDir:$(dvxListing | sha1sum | cut -d' ' -f1)" compPin="dir:$dvxDir:dvx:$([ -f "$dvxRuntimeZip" ] && sha256Of "$dvxRuntimeZip"):sdk:$(dvxSdkPin)"
compInstall() { compInstall() {
local part local tmp
[ -f "$dvxBin/dvx.exe" ] || die "$dvxDir is not a built DVX tree (no bin/dvx.exe); build DVX there, or point --dvx-dir at one" [ -f "$dvxRuntimeZip" ] || die "$dvxDir has no dist/dvx.zip; make DVX's distribution there, or point --dvx-dir at a DVX tree that has one"
[ -f "$dvxBin/$dvxSdk/sdk.mk" ] || die "$dvxDir has no SDK (bin/sdk/sdk.mk); build DVX there first"
rm -rf "$compDir" rm -rf "$compDir"
mkdir -p "$compDir" extractArchive "$dvxRuntimeZip" "$compDir"
for part in $dvxRuntime $dvxSdk; do if [ -f "$dvxSdkZip" ]; then
[ -e "$dvxBin/$part" ] || die "$dvxBin/$part is missing; is the DVX build complete?" tmp="$(freshBuildDir "$compId")"
cp -a "$dvxBin/$part" "$compDir/" extractArchive "$dvxSdkZip" "$tmp"
done [ -f "$tmp/$dvxSdkZipRoot/sdk.mk" ] || die "$dvxSdkZip has no $dvxSdkZipRoot/sdk.mk"
mv "$tmp/$dvxSdkZipRoot" "$compDir/$dvxSdk"
dropBuildDir "$compId"
else
[ -f "$dvxSdkTree/sdk.mk" ] || die "$dvxDir has neither dist/sdk.zip nor a built SDK (bin/sdk/sdk.mk)"
warn "$dvxDir has no dist/sdk.zip: copying its built SDK (bin/sdk) instead"
cp -a "$dvxSdkTree" "$compDir/$dvxSdk"
fi
} }
@ -63,8 +75,8 @@ compInstalled() {
} }
# Build a one-window DVX program with the copied SDK and JoeyLib's DJGPP: # Build a one-window DVX program with the SDK and JoeyLib's DJGPP: proves the
# proves the SDK, its host tools and the compiler work together. # SDK, its host tools and the compiler work together.
compVerify() { compVerify() {
local tmp local tmp
local status=1 local status=1

View file

@ -48,11 +48,13 @@
# unit 0) and DOS's (and DVX's) COM1. # unit 0) and DOS's (and DVX's) COM1.
# #
# A DVX program runs in DOSBox Staging on a copy of the DVX the installer # A DVX program runs in DOSBox Staging on a copy of the DVX the installer
# took from a DVX tree (install dvx), which boots straight into it: the # unpacked from a DVX tree's dist/dvx.zip (install dvx), which boots
# program is DVX's desktop program, in its window on a 1024x768 desktop # straight into it: the program is DVX's desktop program, in its window on a
# (640x480 with --headless). Interactive runs keep that DVX in # 1024x768 desktop (640x480 with --headless). Game > Full Screen, or Alt
# build/dvx/run, so what the program saves (in DVX's CONFIG folder) is # with the keypad's Enter (DOSBox keeps Alt+Enter for its own full screen),
# still there next time. The link is raw TCP, without telnet negotiation. # puts it on the whole screen. Interactive runs keep that DVX in
# build/dvx/run, so what the program saves (in DVX's CONFIG folder), and
# whether it was left on the whole screen, is still there next time. The link is raw TCP, without telnet negotiation.
# socat relays it where the emulator cannot make it itself: the ST, the # socat relays it where the emulator cannot make it itself: the ST, the
# Amiga with --serial, and the IIgs and X68000 with --serial-listen. When # Amiga with --serial, and the IIgs and X68000 with --serial-listen. When
# --serial's server is not there, joey-run stops, except on the IIgs and # --serial's server is not there, joey-run stops, except on the IIgs and

View file

@ -1,10 +1,11 @@
# DVX build rules: every example as a DVX program (.app). # DVX build rules: every example as a DVX program (.app).
# #
# A JoeyLib program on DVX runs in a window of the DVX desktop (src/dvx). The # A JoeyLib program on DVX runs in a window of the DVX desktop, or on the
# library, libjoey.a, is built here with JoeyLib's own DJGPP (DVX is built # whole screen (src/dvx). The library, libjoey.a, is built here with
# with the same gcc 12.2.0) against the SDK the installer copied from a DVX # JoeyLib's own DJGPP (DVX is built with the same gcc 12.2.0) against the SDK
# tree into toolchains/opt/dvx (DVX_SDK); the stage and the graphics are the # the installer unpacked from a DVX tree's dist/sdk.zip into
# DOS port's (src/x86, the chunky generics). Each example is then a DVX SDK # toolchains/opt/dvx/sdk (DVX_SDK); the stage and the graphics are the DOS
# port's (src/x86, the chunky generics). Each example is then a DVX SDK
# project in build/dvx/apps/<name>/ -- symlinks to its sources and to the # project in build/dvx/apps/<name>/ -- symlinks to its sources and to the
# port's entry (src/dvx/dvxApp.c), a generated one-topic help file and # port's entry (src/dvx/dvxApp.c), a generated one-topic help file and
# resource list -- built and packaged by the SDK's own sdk.mk (KIND = app, # resource list -- built and packaged by the SDK's own sdk.mk (KIND = app,
@ -207,6 +208,8 @@ app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data)
'$$($(1)_HELP)' \ '$$($(1)_HELP)' \
'' \ '' \
'$$($(1)_TITLE) is a JoeyLib example program. It runs in its own window at a virtual 70 frames a second; while the window is active every key goes to it except Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close it). Game > Exit also closes it.' \ '$$($(1)_TITLE) is a JoeyLib example program. It runs in its own window at a virtual 70 frames a second; while the window is active every key goes to it except Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close it). Game > Exit also closes it.' \
'' \
'Game > Full Screen, or Alt+Enter, puts it on the whole screen at 320x200 in 256 colours, and back in its window. It starts the way it was left. Alt+Tab or Ctrl+Esc brings the desktop back while it runs on; coming back to it gives it the whole screen again.' \
| $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).dhs | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).dhs
@printf '%s\n' \ @printf '%s\n' \
'name text "$$($(1)_TITLE)"' \ 'name text "$$($(1)_TITLE)"' \

View file

@ -13,7 +13,13 @@
# The ops/sec are the emulated machine's at DOSBox's full host speed, not a # The ops/sec are the emulated machine's at DOSBox's full host speed, not a
# reference machine's: compare them only run against run. # reference machine's: compare them only run against run.
# #
# --full-screen runs UBER on the whole screen (the port's full-screen
# setting staged as Game > Full Screen leaves it, DVX's exclusive mode at
# 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was
# set: the same hashes, the full-screen present's timings.
#
# scripts/bench-dvx.sh # default 600 s timeout # scripts/bench-dvx.sh # default 600 s timeout
# scripts/bench-dvx.sh --full-screen # on the whole screen
# TIMEOUT=900 scripts/bench-dvx.sh # override # TIMEOUT=900 scripts/bench-dvx.sh # override
set -euo pipefail set -euo pipefail
@ -24,6 +30,13 @@ source "$repo/scripts/lib/dvxGate.sh"
joeyNeed DOSBOX dosbox joeyNeed DOSBOX dosbox
joeyNeed DVX_ROOT dvx joeyNeed DVX_ROOT dvx
fullScreen=0
if [ "${1:-}" = "--full-screen" ]; then
fullScreen=1
elif [ $# -gt 0 ]; then
echo "usage: scripts/bench-dvx.sh [--full-screen]" >&2
exit 2
fi
timeoutSeconds=${TIMEOUT:-600} timeoutSeconds=${TIMEOUT:-600}
# The emulator's hard cap, a little past the wait, whatever this script does. # The emulator's hard cap, a little past the wait, whatever this script does.
wallSeconds=$((timeoutSeconds + 30)) wallSeconds=$((timeoutSeconds + 30))
@ -44,6 +57,9 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
dvxStage "$work/c" "$(dvxAppDir uber)" dvxStage "$work/c" "$(dvxAppDir uber)"
if [ "$fullScreen" = 1 ]; then
dvxFullScreen "$work/c" uber
fi
dvxDosboxConf "$work/dosbox.conf" "$work/c" dvxDosboxConf "$work/dosbox.conf" "$work/c"
mkdir -p "$work/xdg" mkdir -p "$work/xdg"
rm -f "$out" rm -f "$out"
@ -76,6 +92,11 @@ joeyStop "$dosbox" "$guard" 2> /dev/null
dosbox="" dosbox=""
guard="" guard=""
if [ "$fullScreen" = 1 ] && ! dvxDvxLog "$work/c" 200 | grep -q "$DVX_FULL_SCREEN_LOG"; then
echo "bench-dvx: FAIL -- --full-screen, but DVX.LOG has no \"$DVX_FULL_SCREEN_LOG\" (DVX.LOG follows)" >&2
dvxDvxLog "$work/c" >&2
exit 1
fi
ops=$(grep -c "ops/sec" "$out" || true) ops=$(grep -c "ops/sec" "$out" || true)
if ! grep -q "$doneLine" "$out"; then if ! grep -q "$doneLine" "$out"; then
echo "bench-dvx: WARNING -- timed out before the completion line (partial capture)" >&2 echo "bench-dvx: WARNING -- timed out before the completion line (partial capture)" >&2

View file

@ -1,7 +1,7 @@
# Shared by the DVX gates (scripts/verify-dvx-*.sh, scripts/bench-dvx.sh) and # Shared by the DVX gates (scripts/verify-dvx-*.sh, scripts/bench-dvx.sh) and
# joey-run: a DVX installation staged in a scratch folder -- a copy of the # joey-run: a DVX installation staged in a scratch folder -- a copy of the
# runtime the installer took from a DVX tree (toolchains/opt/dvx, without # runtime the installer unpacked from DVX's dist/dvx.zip (toolchains/opt/dvx,
# its SDK) plus JoeyLib programs from build/dvx/bin -- whose DVX.INI makes # without its SDK) plus JoeyLib programs from build/dvx/bin -- whose DVX.INI makes
# one of them the desktop, so DVX boots straight into it; and the DOSBox # one of them the desktop, so DVX boots straight into it; and the DOSBox
# Staging run of that folder as drive C:. Nothing here writes to the DVX # Staging run of that folder as drive C:. Nothing here writes to the DVX
# project or to toolchains/opt/dvx. Source after joeyEnv.sh. # project or to toolchains/opt/dvx. Source after joeyEnv.sh.
@ -11,6 +11,14 @@ DVX_BIN="$JOEY_REPO/build/dvx/bin"
# The folder a program installs in under apps/, and the name of its log. # The folder a program installs in under apps/, and the name of its log.
DVX_VENDOR=joeylib DVX_VENDOR=joeylib
DVX_LOG_NAME=joeylog.txt DVX_LOG_NAME=joeylog.txt
# The port's own settings file in a program's settings folder, and its
# full-screen switch (src/dvx/hal.c).
DVX_SETTINGS_FILE=JOEYLIB.INI
DVX_SETTINGS_SECTION=display
DVX_SETTINGS_FULL_SCREEN=fullScreen
# DVX.LOG's line when a program's full-screen mode is set (the port asks for
# the stage's size at 8 bits a pixel).
DVX_FULL_SCREEN_LOG="Video: full screen 320x200 at 8 bpp"
# The runtime parts of toolchains/opt/dvx (all but the SDK). # The runtime parts of toolchains/opt/dvx (all but the SDK).
DVX_RUNTIME="dvx.exe config system libs widgets apps" DVX_RUNTIME="dvx.exe config system libs widgets apps"
# The desktop the gates run: 640x480 in 16-bit colour, DVX's own DOS test # The desktop the gates run: 640x480 in 16-bit colour, DVX's own DOS test
@ -69,6 +77,21 @@ dvxStage() {
} }
# dvxFullScreen DIR PROGRAM -- have PROGRAM start on the whole screen in the
# DVX staged in DIR: the port's own setting (JOEYLIB.INI, [display]
# fullScreen, in the program's settings folder), as Game > Full Screen leaves
# it. DOS names are case-blind, so an existing folder of any case is used.
dvxFullScreen() {
local conf
conf="$(find "$1/DVX" -ipath "*/config/apps/$DVX_VENDOR/$2" -type d -print -quit 2>/dev/null)"
if [ -z "$conf" ]; then
conf="$1/DVX/config/apps/$DVX_VENDOR/$2"
mkdir -p "$conf"
fi
printf '[%s]\r\n%s = true\r\n' "$DVX_SETTINGS_SECTION" "$DVX_SETTINGS_FULL_SCREEN" > "$conf/$DVX_SETTINGS_FILE"
}
# dvxIniSet INI DESKTOP -- DVX.INI for a run: DESKTOP as the desktop program, # dvxIniSet INI DESKTOP -- DVX.INI for a run: DESKTOP as the desktop program,
# no first-run Welcome window, the gate video mode, and no desktop sounds (a # no first-run Welcome window, the gate video mode, and no desktop sounds (a
# recording gate must hear only the program). DOS line ends are kept. # recording gate must hear only the program). DOS line ends are kept.
@ -158,6 +181,18 @@ dvxAwaitLog() {
} }
# dvxKeysTyped LOG TARGET -- KEYS's typed line in LOG (its joeylog.txt: the
# hex of every character it received) must be TARGET's bytes.
dvxKeysTyped() {
local typed
local want
typed="$(tr -d '\r' < "$1" | sed -n 's/^typed=//p' | tail -1)"
want="$(printf '%s' "$2" | od -An -tx1 | tr -d ' \n' | tr 'a-f' 'A-F')"
echo "$DVX_GATE: log typed=$typed"
[ "$typed" = "$want" ]
}
# dvxDvxLog DIR -- print the tail of the staged DVX's own log (DVX.LOG), the # dvxDvxLog DIR -- print the tail of the staged DVX's own log (DVX.LOG), the
# place a program that failed to load is explained. # place a program that failed to load is explained.
dvxDvxLog() { dvxDvxLog() {

98
scripts/lib/keysRow.py Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""keysRow.py - read KEYS's verification row off a screenshot.
KEYS (examples/keys) draws every character it received on its row 198 as
pixel nibbles: the sentinel A 5 A 5, the count byte, a zero byte, then each
character, two pixels a byte, high nibble first. This finds that row in a
screenshot (on a desktop the window is wherever DVX put it) and checks it
holds exactly TARGET.
keysRow.py PNG TARGET [--size WxH] [--at X,Y]
--size: the screenshot must be that size (the full-screen mode, not the
desktop). --at: the row must start exactly there (the stage's own row 198
at 1x on a full screen). Prints the row it found; exit 0 when it is right.
"""
import sys
from PIL import Image
# KEYS palette 0 (buildPalette): a gray ramp with four demo colours and red
# at 15, 12-bit $0RGB, each channel widened x17 as the DVX port's tables do.
PAL12 = [(i << 8) | (i << 4) | i for i in range(16)]
PAL12[0], PAL12[1], PAL12[2], PAL12[3] = 0x000, 0x333, 0x0F0, 0xFFF
PAL12[15] = 0xF00
PAL_RGB = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in PAL12]
# A 16-bit desktop (5-6-5) is off by at most 8 a channel after the round
# trip, the 6-bit VGA DAC by at most 3, while the palette entries sit 17
# apart: nearest is exact.
MAX_DIST = 3 * 9 ** 2
SENTINEL = (0xA, 0x5, 0xA, 0x5)
def main(argv):
if len(argv) < 3:
sys.stderr.write(__doc__)
return 2
png, target = argv[1], argv[2]
size = None
at = None
args = argv[3:]
while args:
if args[0] == "--size" and len(args) > 1:
size = tuple(int(n) for n in args[1].split("x"))
elif args[0] == "--at" and len(args) > 1:
at = tuple(int(n) for n in args[1].split(","))
else:
sys.stderr.write(__doc__)
return 2
args = args[2:]
img = Image.open(png).convert("RGB")
w, h = img.size
px = img.load()
if size is not None and img.size != size:
print("keysRow: FAIL - %s is %dx%d, want %dx%d" % (png, w, h, size[0], size[1]))
return 1
nearest = {}
def nib(x, y):
rgb = px[x, y]
if rgb not in nearest:
best, bestDist = 0, 1 << 30
for n, pal in enumerate(PAL_RGB):
d = sum((c - p) ** 2 for c, p in zip(rgb, pal))
if d < bestDist:
best, bestDist = n, d
nearest[rgb] = best if bestDist <= MAX_DIST else -1
return nearest[rgb]
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
found = None
for y in range(h):
for x in range(w - 2 * len(expect)):
if tuple(nib(x + i, y) for i in range(len(SENTINEL))) == SENTINEL:
row = bytes(((nib(x + 2 * i, y) & 0xF) << 4) | (nib(x + 2 * i + 1, y) & 0xF) for i in range(len(expect)))
if row[:4] == expect[:4]:
found = (x, y, row)
break
if found:
break
if not found:
print("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen" % len(target))
return 1
x, y, row = found
print("keysRow: row at (%d,%d)=%s" % (x, y, row.hex().upper()))
if row != expect:
print("keysRow: FAIL - screen row %s, want %s" % (row.hex().upper(), expect.hex().upper()))
return 1
if at is not None and (x, y) != at:
print("keysRow: FAIL - the row is at (%d,%d), want (%d,%d)" % (x, y, at[0], at[1]))
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View file

@ -13,6 +13,17 @@ works too).
WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP FAIL WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP FAIL
line; exit 0 on pass, 1 on fail. line; exit 0 on pass, 1 on fail.
--dtr-check (TCP, with DOSBox's nullmodem in transparent:0 mode, which puts
the guest's modem lines in the byte stream as they change: 0xFF 0xFF is a
data 0xFF, 0xFF <b> the lines, bit 0 RTS, bit 1 DTR) then runs SERTEST's
DTR check: it sends DTR_CHECK, acknowledges each of SERTEST's marker lines
(so SERTEST changes a line only once the marker before has gone out whole),
and reads the line states against the markers. jlSerialSetDtr(false) and
(true) must reach the line, closing and opening the port again must keep
DTR up, and after a hang-up (DTR dropped, then the port closed) DTR must
stay down for HANGUP_WATCH_SECONDS. Prints one DTR PASS / DTR FAIL line
more.
""" """
import os import os
@ -30,6 +41,59 @@ CONNECT_RETRY_SECONDS = 0.5
READ_SIZE = 256 READ_SIZE = 256
SHOWN_BYTES = 80 SHOWN_BYTES = 80
# SERTEST's DTR check (examples/sertest/sertest.c).
DTR_CHECK = b"\x04"
DTR_ACK = b"\x06"
MARKS = (("off", b"JLDTR-OFF\r\n"), ("on", b"JLDTR-ON\r\n"), ("reopened", b"JLDTR-REOPENED\r\n"), ("hangup", b"JLDTR-HANGUP\r\n"))
HANGUP_WATCH_SECONDS = 3.0
# DOSBox nullmodem transparent:0 framing.
LINE_ESCAPE = 0xFF
LINE_DTR = 0x02
class LineDecoder:
"""Splits a transparent:0 stream into data and the guest's DTR states,
each state kept with the count of data bytes that came before it."""
def __init__(self):
self.pending = b""
self.dataCount = 0
self.dtr = []
def feed(self, raw):
buf = self.pending + raw
data = bytearray()
i = 0
while i < len(buf):
if buf[i] != LINE_ESCAPE:
data.append(buf[i])
i += 1
continue
if i + 1 >= len(buf):
break
if buf[i + 1] == LINE_ESCAPE:
data.append(LINE_ESCAPE)
else:
self.dtr.append((self.dataCount + len(data), (buf[i + 1] & LINE_DTR) != 0))
i += 2
self.pending = buf[i:]
self.dataCount += len(data)
return bytes(data)
def dtrBefore(self, position):
"""DTR as it was when the data byte at position arrived (None: never reported)."""
state = None
for at, dtr in self.dtr:
if at > position:
break
state = dtr
return state
def dtrChanges(self, first, last, value):
"""Whether DTR was reported as value after data byte first, up to last."""
return any(first < at <= last and dtr == value for at, dtr in self.dtr)
def acceptOne(port, wall): def acceptOne(port, wall):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -64,8 +128,57 @@ def openPty(pathFile):
return master return master
def dtrCheck(send, receive, wall, start, lines, got):
# After the loopback: the stream so far is got; lines has decoded it.
base = len(got)
send(DTR_CHECK)
marks = {}
hangupAt = None
while time.time() - start < wall:
if hangupAt is not None and time.time() - hangupAt >= HANGUP_WATCH_SECONDS:
break
data = receive()
if data is None:
break
got += data
# Acknowledge each marker line as it completes; the last starts the
# watch.
while len(marks) < len(MARKS):
name, mark = MARKS[len(marks)]
at = got.find(mark, base)
if at < 0:
break
marks[name] = at
send(DTR_ACK)
if hangupAt is None and len(marks) == len(MARKS):
hangupAt = time.time()
if len(marks) < len(MARKS):
print("DTR FAIL: no %r from SERTEST; stream=%r" % (MARKS[len(marks)][1], got[base:base + SHOWN_BYTES]))
return 1
if time.time() - hangupAt < HANGUP_WATCH_SECONDS:
print("DTR FAIL: out of time before the watch after the hang-up ended")
return 1
fails = []
if lines.dtrBefore(marks["off"]) is not False:
fails.append("jlSerialSetDtr(false) did not drop DTR")
if lines.dtrBefore(marks["on"]) is not True:
fails.append("jlSerialSetDtr(true) did not raise DTR")
if lines.dtrChanges(marks["on"], marks["reopened"], False):
fails.append("closing and opening the port dropped DTR")
if lines.dtrBefore(marks["hangup"]) is not False:
fails.append("jlSerialSetDtr(false) after the reopen did not drop DTR")
if lines.dtrChanges(marks["hangup"], lines.dataCount, True):
fails.append("closing the port after the hang-up raised DTR")
if fails:
print("DTR FAIL: %s; DTR states (data byte, DTR) since the check: %r" % ("; ".join(fails), [x for x in lines.dtr if x[0] >= base]))
return 1
print("DTR PASS: SetDtr(false/true) reached the line, close+open kept DTR up, a hang-up stayed down %.0f s after the close" % HANGUP_WATCH_SECONDS)
return 0
def loop(send, receive, wall, start, banner): def loop(send, receive, wall, start, banner):
# send(bytes); receive() -> bytes, b"" for "nothing yet", None at EOF. # send(bytes); receive() -> bytes, b"" for "nothing yet", None at EOF.
# Returns the exit status and the stream.
got = b"" got = b""
sentAt = None sentAt = None
lastSend = 0.0 lastSend = 0.0
@ -81,13 +194,15 @@ def loop(send, receive, wall, start, banner):
got += data got += data
if sentAt is not None and PROBE in got[sentAt:]: if sentAt is not None and PROBE in got[sentAt:]:
print("LOOP PASS: banner %r seen, probe echoed; stream=%r" % (banner, got[:SHOWN_BYTES])) print("LOOP PASS: banner %r seen, probe echoed; stream=%r" % (banner, got[:SHOWN_BYTES]))
return 0 return 0, got
print("LOOP FAIL: stream=%r" % got[:SHOWN_BYTES + 40]) print("LOOP FAIL: stream=%r" % got[:SHOWN_BYTES + 40])
return 1 return 1, got
def main(argv): def main(argv):
if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty"): withDtrCheck = "--dtr-check" in argv
argv = [arg for arg in argv if arg != "--dtr-check"]
if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty"):
sys.stderr.write(__doc__) sys.stderr.write(__doc__)
return 2 return 2
mode = argv[1] mode = argv[1]
@ -107,7 +222,7 @@ def main(argv):
except OSError: except OSError:
return b"" return b""
return loop(lambda data: os.write(master, data), receive, wall, start, banner) return loop(lambda data: os.write(master, data), receive, wall, start, banner)[0]
port = int(argv[2]) port = int(argv[2])
conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start) conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start)
@ -116,6 +231,8 @@ def main(argv):
return 1 return 1
conn.settimeout(POLL_SECONDS) conn.settimeout(POLL_SECONDS)
lines = LineDecoder()
def receive(): def receive():
try: try:
data = conn.recv(READ_SIZE) data = conn.recv(READ_SIZE)
@ -123,9 +240,14 @@ def main(argv):
return b"" return b""
except OSError: except OSError:
return None return None
return data if data else None if not data:
return None
return lines.feed(data) if withDtrCheck else data
return loop(conn.sendall, receive, wall, start, banner) status, got = loop(conn.sendall, receive, wall, start, banner)
if status != 0 or not withDtrCheck:
return status
return dtrCheck(conn.sendall, receive, wall, start, lines, got)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -10,6 +10,9 @@
# tests/goldens/uber (x68000 against the IIgs's, its own gate; # tests/goldens/uber (x68000 against the IIgs's, its own gate;
# dvx against the DOS port's, whose stage it shows) # dvx against the DOS port's, whose stage it shows)
# visual iigs: every launchable example on joey.2mg boots and draws # visual iigs: every launchable example on joey.2mg boots and draws
# dvx: full screen -- UBER's hashes through a suspend and a
# resume, and KEYS's row read off the 320x200 VGA screen, then
# in its window, then on the whole screen again
# tilebank each port: examples/tilebank loads the port's own .tbk bake # tilebank each port: examples/tilebank loads the port's own .tbk bake
# of the host fixture both ways, pixel-exact and with the host # of the host fixture both ways, pixel-exact and with the host
# gate's surface hash, and refuses every other target's bake (the # gate's surface hash, and refuses every other target's bake (the
@ -97,6 +100,7 @@ golden amiga uber TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh &&
golden dos uber scripts/bench-dos.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dos/bin/JOEYLOG.TXT golden dos uber scripts/bench-dos.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dos/bin/JOEYLOG.TXT
golden dvx uber scripts/bench-dvx.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dvx/bin/joeylog.txt golden dvx uber scripts/bench-dvx.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dvx/bin/joeylog.txt
visual iigs examples scripts/verify-iigs-all.sh visual iigs examples scripts/verify-iigs-all.sh
visual dvx full scripts/verify-dvx-fullscreen.sh
tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1 tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1
tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh
tilebank atarist tilebank scripts/verify-atarist-tilebank.sh tilebank atarist tilebank scripts/verify-atarist-tilebank.sh

226
scripts/verify-dvx-fullscreen.sh Executable file
View file

@ -0,0 +1,226 @@
#!/usr/bin/env bash
# verify-dvx-fullscreen.sh - Full-screen gate for DVX: a program on the whole
# screen in DVX's exclusive mode (src/dvx/hal.c), started there by the port's
# own setting as Game > Full Screen leaves it (dvxFullScreen), and read back
# from DOSBox's raw screenshots of the VGA screen. Two runs of a scratch DVX
# in DOSBox Staging under Xvfb:
# 1. UBER on the whole screen, suspended part way (Ctrl+Esc: DVX's Task
# Manager takes the focus, the desktop comes back and UBER presents into
# its window) and resumed (the Task Manager's Switch To): the
# screenshots must be the 320x200 mode, the desktop, then 320x200 again;
# DVX.LOG must show the mode set twice; and UBER's 51 hashes must equal
# the DOS port's (tests/goldens/uber/dos.txt): the stage is DOS's
# wherever it is shown.
# 2. KEYS on the whole screen: the typed string must reach jlInputGetChar
# (its log), and its verification row must read back off the 320x200
# screen at exactly the stage's own (0,198) (scripts/lib/keysRow.py):
# the full-screen present, its palette and its placement. Alt+keypad
# Enter (DOSBox keeps Alt+Enter for its own full screen) takes KEYS back
# to its window, where the row must be found on the desktop; the same
# keys again, and the row must be at (0,198) once more. The setting must
# still say full screen.
#
# Usage: scripts/verify-dvx-fullscreen.sh
# Env: DVX_FULLSCREEN_SHOTS also copy the screenshots into this folder
# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component;
# the uber and keys programs (make dvx).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
source "$repo/scripts/lib/dvxGate.sh"
joeyNeed DOSBOX dosbox
joeyNeed DVX_ROOT dvx
target="192.168.1.10:6510"
wall=180 # hard cap on each DOSBox run, whatever happens to this script
readySeconds=60 # DVX boot until its shell reports ready
settleSeconds=3 # then the program taking the screen
stepSeconds=2 # a key, then DVX and the program acting on it
shotSeconds=5 # Ctrl+F5 until DOSBox has written the screenshot
holdSeconds=0.3 # a key held that long: a quick tap can fall between two polls
uberSeconds=120 # UBER's run until its done line
logSeconds=30 # Escape until KEYS's typed line is in its log
fullSize=320x200 # DVX's full-screen mode for the stage: VGA mode 13h here
desktopSize="${DVX_VIDEO_W}x${DVX_VIDEO_H}"
rowAt=0,198 # KEYS's verification row at 1x: its place on the stage
uberDone="press any key to exit"
dvxNeedBuild uber keys
work=$(mktemp -d -t joeylib-dvx-fullscreen.XXXXXX)
guard=""
dosbox=""
xvfb=""
win=""
fails=""
# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed".
cleanup() {
joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null
rm -rf "$work"
}
trap cleanup EXIT
# boot PROGRAM -- DVX staged in $work/PROGRAM with PROGRAM its desktop, on the
# whole screen from the start; sets guard, dosbox and win.
boot() {
local dir="$work/$1"
local waited
dvxStage "$dir/c" "$(dvxAppDir "$1")"
dvxFullScreen "$dir/c" "$1"
dvxDosboxConf "$dir/dosbox.conf" "$dir/c"
printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$dir/shots" >> "$dir/dosbox.conf"
mkdir -p "$dir/xdg" "$dir/shots"
XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$dir/dosbox.conf" > "$dir/dosbox.log" 2>&1 &
guard=$!
dosbox=$(joeyChild "$guard")
for ((waited = 0; waited < readySeconds; waited++)); do
if dvxDvxLog "$dir/c" 200 | grep -q "DVX Shell ready"; then
sleep "$settleSeconds"
win=$(joeyFindWindow "$display" "$dosbox")
[ -n "$win" ] || return 1
DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true
return 0
fi
joeyAlive "$guard" || return 1
sleep 1
done
return 1
}
# fail WHAT -- note a failed check; the gate goes on to report the rest.
fail() {
echo "verify-dvx-fullscreen: FAIL - $1"
fails="${fails:+$fails; }$1"
}
# fullScreenSets PROGRAM -- how often DVX.LOG says the full-screen mode was set.
fullScreenSets() {
dvxDvxLog "$work/$1/c" 400 | grep -c "$DVX_FULL_SCREEN_LOG"
}
# hold KEY... -- press the keys in order, each held holdSeconds before the
# next (a chord's modifier goes down first), release them in reverse, then
# give DVX and the program time to act.
hold() {
local k
local i
for k in "$@"; do
DISPLAY="$display" xdotool keydown --window "$win" "$k"
sleep "$holdSeconds"
done
for ((i = $#; i >= 1; i--)); do
DISPLAY="$display" xdotool keyup --window "$win" "${!i}"
done
sleep "$stepSeconds"
}
# shot PROGRAM NAME SIZE -- a DOSBox screenshot (Ctrl+F5: the raw VGA picture)
# as $work/NAME.png, which must be SIZE.
shot() {
local png
local size
local waited
DISPLAY="$display" xdotool key --window "$win" ctrl+F5
for ((waited = 0; waited < shotSeconds; waited++)); do
sleep 1
png=$(ls "$work/$1/shots/"*.png 2> /dev/null | head -1)
if [ -n "$png" ]; then
mv "$png" "$work/$2.png"
size=$(python3 -c 'import sys; from PIL import Image; print("%dx%d" % Image.open(sys.argv[1]).size)' "$work/$2.png")
echo "verify-dvx-fullscreen: $2: $size"
[ "$size" = "$3" ] || fail "$2 is $size, want $3"
return 0
fi
done
fail "no screenshot for $2"
}
# stop -- end the DOSBox run.
stop() {
joeyStop "$dosbox" "$guard" 2> /dev/null
dosbox=""
guard=""
}
display=":$((RANDOM % 400 + 3700))"
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 &
xvfb=$!
sleep 1
# 1. UBER: the hashes on the whole screen, through a suspend and a resume.
if ! boot uber; then
echo "verify-dvx-fullscreen: FAIL - DVX with UBER did not come up (DVX.LOG follows)" >&2
dvxDvxLog "$work/uber/c" >&2
exit 1
fi
shot uber uberFull "$fullSize"
hold ctrl Escape
shot uber uberSuspended "$desktopSize"
hold alt s
shot uber uberResumed "$fullSize"
log=$(dvxAwaitLog "$work/uber/c" uber "$uberDone" "$uberSeconds" "$guard")
sets=$(fullScreenSets uber)
stop
echo "verify-dvx-fullscreen: UBER: full-screen mode set $sets times"
[ "$sets" -ge 2 ] || fail "UBER: DVX set the full-screen mode $sets times, want 2 (start, resume)"
if [ -z "$log" ]; then
fail "UBER wrote no \"$uberDone\" within ${uberSeconds}s"
else
tr -d '\r' < "$log" > "$work/uber.txt"
if hashes=$("$repo/tools/diff-uber-hashes" "$repo/tests/goldens/uber/dos.txt" "$work/uber.txt"); then
echo "verify-dvx-fullscreen: UBER hashes: $(printf '%s\n' "$hashes" | tail -1)"
else
printf '%s\n' "$hashes"
fail "UBER's hashes on the whole screen differ from the DOS port's"
fi
fi
# 2. KEYS: the typed row read off the screen, full screen, window, full again.
if ! boot keys; then
echo "verify-dvx-fullscreen: FAIL - DVX with KEYS did not come up (DVX.LOG follows)" >&2
dvxDvxLog "$work/keys/c" >&2
exit 1
fi
DISPLAY="$display" xdotool type --window "$win" --delay 150 -- "$target"
sleep "$stepSeconds"
shot keys keysFull "$fullSize"
python3 "$repo/scripts/lib/keysRow.py" "$work/keysFull.png" "$target" --at "$rowAt" || fail "KEYS's row on the whole screen"
hold alt KP_Enter
shot keys keysWindow "$desktopSize"
python3 "$repo/scripts/lib/keysRow.py" "$work/keysWindow.png" "$target" || fail "KEYS's row in its window after leaving the whole screen"
hold alt KP_Enter
shot keys keysFullAgain "$fullSize"
python3 "$repo/scripts/lib/keysRow.py" "$work/keysFullAgain.png" "$target" --at "$rowAt" || fail "KEYS's row on the whole screen again"
hold Escape
log=$(dvxAwaitLog "$work/keys/c" keys '^typed=' "$logSeconds" "$guard")
sets=$(fullScreenSets keys)
stop
echo "verify-dvx-fullscreen: KEYS: full-screen mode set $sets times"
[ "$sets" -ge 2 ] || fail "KEYS: DVX set the full-screen mode $sets times, want 2 (start, Alt+Enter)"
if [ -z "$log" ]; then
fail "KEYS logged no typed line within ${logSeconds}s of Escape"
else
dvxKeysTyped "$log" "$target" || fail "KEYS's typed log"
fi
setting=$(find "$work/keys/c/DVX" -ipath "*/config/apps/$DVX_VENDOR/keys/$DVX_SETTINGS_FILE" -print -quit)
tr -d '\r' < "$setting" 2> /dev/null | grep -qi "^$DVX_SETTINGS_FULL_SCREEN *= *true" || fail "the setting does not say full screen after switching back ($setting)"
if [ -n "${DVX_FULLSCREEN_SHOTS:-}" ]; then
mkdir -p "$DVX_FULLSCREEN_SHOTS"
cp "$work"/*.png "$DVX_FULLSCREEN_SHOTS/"
fi
if [ -n "$fails" ]; then
echo "verify-dvx-fullscreen: FAIL - $fails"
exit 1
fi
echo "verify-dvx-fullscreen: PASS (UBER 51/51 on the whole screen through a suspend and resume; KEYS's typed row read off the 320x200 screen at (0,198), in its window, and full screen again)"

View file

@ -8,7 +8,8 @@
# verification scanline (row 198: sentinel, count, every received # verification scanline (row 198: sentinel, count, every received
# character as two pixel nibbles) somewhere -- the window's content, # character as two pixel nibbles) somewhere -- the window's content,
# expanded from the stage through the port's palette tables into DVX's # expanded from the stage through the port's palette tables into DVX's
# 16-bit desktop -- decoding to exactly the typed bytes; # 16-bit desktop -- decoding to exactly the typed bytes
# (scripts/lib/keysRow.py);
# 2. the typed history: after Escape, KEYS logs every received character # 2. the typed history: after Escape, KEYS logs every received character
# as hex to its joeylog.txt (in its settings folder), which must equal # as hex to its joeylog.txt (in its settings folder), which must equal
# the typed ASCII. # the typed ASCII.
@ -105,73 +106,11 @@ if [ -z "$log" ]; then
exit 1 exit 1
fi fi
python3 - "$png" "$target" "$work/joeylog.txt" <<'PY' fails=""
import sys python3 "$repo/scripts/lib/keysRow.py" "$png" "$target" || fails="the present"
from PIL import Image dvxKeysTyped "$work/joeylog.txt" "$target" || fails="${fails:+$fails, }the log"
if [ -n "$fails" ]; then
png, target, logPath = sys.argv[1], sys.argv[2], sys.argv[3] echo "verify-dvx-input: FAIL - $fails"
fails = [] exit 1
fi
# KEYS palette 0 (buildPalette): a gray ramp with four demo colours and red echo "verify-dvx-input: PASS (typed \"$target\": all ${#target} chars reached jlInputGetChar, and the present shows them)"
# at 15, 12-bit $0RGB, each channel widened x17 as the port's tables do.
pal12 = [(i << 8) | (i << 4) | i for i in range(16)]
pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF
pal12[15] = 0xF00
palRgb = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in pal12]
# The desktop is 16-bit (5-6-5): a channel is off by at most 8 after the
# round trip, while the palette entries sit 17 apart; nearest is exact.
maxDist = 3 * 9 ** 2
img = Image.open(png).convert("RGB")
w, h = img.size
px = img.load()
nearest = {}
def nib(x, y):
rgb = px[x, y]
if rgb not in nearest:
best, bestDist = 0, 1 << 30
for n, pal in enumerate(palRgb):
d = sum((c - p) ** 2 for c, p in zip(rgb, pal))
if d < bestDist:
best, bestDist = n, d
nearest[rgb] = best if bestDist <= maxDist else -1
return nearest[rgb]
# Find the verification row: the sentinel's four nibbles A 5 A 5, then the
# count byte, anywhere on the desktop (the window is wherever DVX put it; at
# the gate's 640x480 it shows the stage at 1x).
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
found = None
for y in range(h):
for x in range(w - 2 * len(expect)):
if nib(x, y) == 0xA and nib(x + 1, y) == 0x5 and nib(x + 2, y) == 0xA and nib(x + 3, y) == 0x5:
row = bytes(((nib(x + 2 * i, y) & 0xF) << 4) | (nib(x + 2 * i + 1, y) & 0xF) for i in range(len(expect)))
if row[:4] == expect[:4]:
found = (x, y, row)
break
if found:
break
if not found:
fails.append("no verification row (A5A5 %02X00) on the screen" % len(target))
else:
x, y, row = found
print("VERIFY-DVX-INPUT row at (%d,%d)=%s" % (x, y, row.hex().upper()))
if row != expect:
fails.append("screen row %s, want %s" % (row.hex().upper(), expect.hex().upper()))
typed = ""
for line in open(logPath, errors="replace"):
if line.startswith("typed="):
typed = line.strip()[len("typed="):]
want = target.encode("ascii").hex().upper()
print("VERIFY-DVX-INPUT log typed=%s" % typed)
if typed != want:
fails.append("log typed=%s, want %s" % (typed, want))
if fails:
print("verify-dvx-input: FAIL - " + "; ".join(fails))
sys.exit(1)
print('verify-dvx-input: PASS (typed "%s": all %d chars reached jlInputGetChar, and the present shows them)' % (target, len(target)))
PY

View file

@ -8,6 +8,13 @@
# serial layer -> the DVX port (src/dvx/serial.c) -> DVX's rs232 library and # serial layer -> the DVX port (src/dvx/serial.c) -> DVX's rs232 library and
# its interrupt -> DOSBox's UART -> host. # its interrupt -> DOSBox's UART -> host.
# #
# Then the DTR check (--dtr-check): the nullmodem runs non-transparent, so
# DOSBox reports the guest's modem lines to the peer in the byte stream, and
# SERTEST drops and raises DTR, closes and reopens the port, then hangs up
# (DTR dropped, port closed). jlSerialSetDtr must reach the line, and
# jlSerialClose must leave DTR as it is, up or down: DVX's serial library
# used to reset the modem lines when it closed a port.
#
# Usage: scripts/verify-dvx-serial.sh # Usage: scripts/verify-dvx-serial.sh
# Env: DVX_SERIAL_PORT TCP port the peer listens on (default 6803) # Env: DVX_SERIAL_PORT TCP port the peer listens on (default 6803)
# Requires: DOSBox Staging, Xvfb, python3, the dvx component; the sertest # Requires: DOSBox Staging, Xvfb, python3, the dvx component; the sertest
@ -22,8 +29,8 @@ joeyNeed DVX_ROOT dvx
program=sertest program=sertest
port="${DVX_SERIAL_PORT:-6803}" port="${DVX_SERIAL_PORT:-6803}"
peerWall=90 # DVX boot, then the banner and the echo peerWall=95 # DVX boot, the banner and the echo, then the DTR check
runSeconds=100 # SERTEST echoes forever: DOSBox ends by timeout runSeconds=110 # SERTEST idles forever after the check: DOSBox ends by timeout
dvxNeedBuild "$program" dvxNeedBuild "$program"
@ -41,10 +48,10 @@ trap cleanup EXIT
dvxStage "$work/c" "$(dvxAppDir "$program")" dvxStage "$work/c" "$(dvxAppDir "$program")"
dvxDosboxConf "$work/dosbox.conf" "$work/c" dvxDosboxConf "$work/dosbox.conf" "$work/c"
printf '\n[serial]\nserial1 = nullmodem server:127.0.0.1 port:%s transparent:1\n' "$port" >> "$work/dosbox.conf" printf '\n[serial]\nserial1 = nullmodem server:127.0.0.1 port:%s transparent:0\n' "$port" >> "$work/dosbox.conf"
mkdir -p "$work/xdg" mkdir -p "$work/xdg"
python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$peerWall" > "$work/peer.log" 2>&1 & python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$peerWall" --dtr-check > "$work/peer.log" 2>&1 &
peerPid=$! peerPid=$!
sleep 1 sleep 1
@ -67,7 +74,7 @@ dosbox=""
guard="" guard=""
cat "$work/peer.log" cat "$work/peer.log"
if [ "$rc" -eq 0 ]; then if [ "$rc" -eq 0 ]; then
echo "verify-dvx-serial: PASS (banner out, probe echoed through COM1 and DVX's serial library)" echo "verify-dvx-serial: PASS (banner out, probe echoed through COM1 and DVX's serial library; DTR follows jlSerialSetDtr and survives jlSerialClose)"
else else
echo "verify-dvx-serial: FAIL (DVX.LOG follows)" >&2 echo "verify-dvx-serial: FAIL (DVX.LOG follows)" >&2
dvxDvxLog "$work/c" >&2 dvxDvxLog "$work/c" >&2

View file

@ -26,6 +26,7 @@
#include "port.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
#include "x86Expand.h"
// ----- Constants ----- // ----- Constants -----
@ -39,9 +40,7 @@
// ----- Prototypes ----- // ----- Prototypes -----
static void crashHandler(int sig); static void crashHandler(int sig);
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst);
static void installCrashLog(void); static void installCrashLog(void);
static void rebuildExpandLut(uint8_t palBase);
static void uploadPalette(const jlSurfaceT *src); static void uploadPalette(const jlSurfaceT *src);
static void uploadPaletteIfNeeded(const jlSurfaceT *src); static void uploadPaletteIfNeeded(const jlSurfaceT *src);
@ -58,72 +57,8 @@ static FILE *gCrashLog = NULL;
// gStagePaletteDirty flag (surfaceInternal.h) says the stage palette // gStagePaletteDirty flag (surfaceInternal.h) says the stage palette
// is unchanged -- see uploadPaletteIfNeeded. // is unchanged -- see uploadPaletteIfNeeded.
// Expand LUT for jlpPresent. Maps a 4bpp-packed source byte to the
// 2-byte little-endian mode-13h output pair for the current scanline's
// SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds
// the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT
// pixel) holds the LOW nibble. Storing lut[byte] as one 16-bit x86
// write therefore lands the left pixel at the lower address, matching
// the old two-byte sequence exactly. Rebuilt only when the SCB-derived
// palBase changes from the previous row (most stages run long runs of
// equal SCB), so the per-row cost is one compare, not a 256-entry fill.
static uint16_t gExpandLut[256];
static uint8_t gExpandLutBase = 0;
static bool gExpandLutValid = false;
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) {
uint8_t palBase;
const uint8_t *srcBytes;
uint32_t *out;
uint16_t quads;
uint16_t i;
palBase = (uint8_t)(src->scb[y] << 4);
if (!gExpandLutValid || palBase != gExpandLutBase) {
rebuildExpandLut(palBase);
}
// Dirty bands are chunky-word aligned (jlpPresent, the only
// caller, computes pixelX = minWord*4 and pixelW = words*4), so x
// and w are multiples of 4: &dst[x] is 32-bit aligned and the
// dword fusion below is tail-free by construction. Two source
// bytes (= 4 nibbles) fuse into ONE aligned 32-bit store straight
// into VGA RAM (perf-hunt-2 rank 6) -- little-endian puts the
// first LUT word in the low half, so the written bytes are
// identical to the old per-pair 16-bit stores. DOSBox's
// instruction-counting core charges the old loop ~3.5
// instructions per VGA byte; halving the loop trips halves that.
srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
out = (uint32_t *)&dst[x];
quads = (uint16_t)(w >> 2);
for (i = 0; i < quads; i++) {
*out++ = (uint32_t)gExpandLut[srcBytes[0]] | ((uint32_t)gExpandLut[srcBytes[1]] << 16);
srcBytes += 2;
}
}
static void rebuildExpandLut(uint8_t palBase) {
uint16_t byte;
uint8_t leftPixel;
uint8_t rightPixel;
for (byte = 0; byte < 256u; byte++) {
// Packed high nibble = LEFT pixel (lower x); packed low nibble
// = RIGHT pixel. The little-endian word puts the LEFT pixel in
// the low byte so a 16-bit store lands it at the lower (even)
// VGA address -- byte-for-byte identical to the old pair store.
leftPixel = (uint8_t)(palBase | (byte >> 4));
rightPixel = (uint8_t)(palBase | (byte & 0x0Fu));
gExpandLut[byte] = (uint16_t)((uint16_t)rightPixel << 8 | (uint16_t)leftPixel);
}
gExpandLutBase = palBase;
gExpandLutValid = true;
}
static void uploadPalette(const jlSurfaceT *src) { static void uploadPalette(const jlSurfaceT *src) {
uint16_t p; uint16_t p;
uint16_t c; uint16_t c;
@ -287,7 +222,7 @@ void jlpPresent(const jlSurfaceT *src) {
// Walk per-row dirty bands: each chunky word holds 4 mode-13h // Walk per-row dirty bands: each chunky word holds 4 mode-13h
// bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4 // bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4
// gives the byte range expandAndWriteLine needs. // gives the byte range x86ExpandRow needs.
// Group test, 4 rows per dword (perf-hunt-2 rank 5, the landed // Group test, 4 rows per dword (perf-hunt-2 rank 5, the landed
// ST/Amiga idle-scan trick): a clean row's minWord is the 0xFF // ST/Amiga idle-scan trick): a clean row's minWord is the 0xFF
// CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX // CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX
@ -324,7 +259,7 @@ void jlpPresent(const jlSurfaceT *src) {
} }
pixelX = (int16_t)((uint16_t)minWord << 2); pixelX = (int16_t)((uint16_t)minWord << 2);
pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2); pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2);
expandAndWriteLine(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]); x86ExpandRow(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]);
gPresentCopiedBytes += pixelW; gPresentCopiedBytes += pixelW;
} }
} }

View file

@ -3,9 +3,10 @@
// A JoeyLib program on DVX is a DVX main-loop program (an .app module): // A JoeyLib program on DVX is a DVX main-loop program (an .app module):
// dvxApp.c, compiled into each program, is its entry; program.c runs the // dvxApp.c, compiled into each program, is its entry; program.c runs the
// program's main and ends it; hal.c owns its window (the stage at a whole // program's main and ends it; hal.c owns its window (the stage at a whole
// scale), the frame clock and the present; input.c the keyboard, mouse and // scale) and full screen, the frame clock and the present; input.c the
// joysticks; audio.c, save.c and serial.c the add-on services. Everything // keyboard, mouse and joysticks; audio.c, save.c and serial.c the add-on
// here is named dvxPort* so nothing collides with DVX's own dvx* names. // services. Everything here is named dvxPort* so nothing collides with
// DVX's own dvx* names.
#ifndef JOEY_DVX_PORT_H #ifndef JOEY_DVX_PORT_H
#define JOEY_DVX_PORT_H #define JOEY_DVX_PORT_H
@ -42,17 +43,25 @@ int32_t dvxPortRun(DxeAppContextT *ctx);
// ----- hal.c ----- // ----- hal.c -----
extern WindowT *gDvxPortWindow; // the program's window; NULL outside jlInit..jlShutdown extern WindowT *gDvxPortWindow; // the program's window; NULL outside jlInit..jlShutdown
extern int32_t gDvxPortScale; // window pixels per stage pixel, each way
// End the program now if the user closed its window (the close box, // End the program now if the user closed its window (the close box,
// Alt+F4, Game > Exit). // Alt+F4, Game > Exit).
void dvxPortEndIfClosed(void); void dvxPortEndIfClosed(void);
// Full screen on if it is off, off if it is on (Game > Full Screen,
// Alt+Enter); the choice is kept for the program's next run.
void dvxPortFullScreenToggle(void);
// Called where a JoeyLib program comes back to the port every frame (the // Called where a JoeyLib program comes back to the port every frame (the
// input poll, the present): yield to the desktop if the program has not // input poll, the present): yield to the desktop if the program has not
// this virtual frame, then dvxPortEndIfClosed. // this virtual frame, then dvxPortEndIfClosed.
void dvxPortService(void); void dvxPortService(void);
// A point in the coordinates the window's onMouse gets (content-area pixels,
// or screen pixels while the program has the whole screen) to the stage
// pixel under it, clamped to the stage.
void dvxPortStagePoint(int32_t *x, int32_t *y);
// ----- input.c: the window's input callbacks, installed by hal.c ----- // ----- input.c: the window's input callbacks, installed by hal.c -----
void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod); void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod);

View file

@ -13,6 +13,7 @@
#include "dvxApp.h" #include "dvxApp.h"
#include "dvxPal.h" #include "dvxPal.h"
#include "dvxPlat.h" #include "dvxPlat.h"
#include "dvxPrefs.h"
#include "dvxVideo.h" #include "dvxVideo.h"
#include "dvxWm.h" #include "dvxWm.h"
#include "shellApp.h" #include "shellApp.h"

View file

@ -1,10 +1,12 @@
// DVX HAL: the stage in a window, the frame clock and the present. // DVX HAL: the stage in a window or on the whole screen, the frame clock and
// the present.
// //
// jlpInit opens the program's window: a game input window (every key but // jlpInit opens the program's window: a game input window (every key but
// Alt+Tab, Ctrl+Esc and Alt+F4 comes to the program) whose content area is // Alt+Tab, Ctrl+Esc and Alt+F4 comes to the program) whose content area is
// the 320x200 stage at the largest whole scale up to DVX_SCALE_MAX that fits // the 320x200 stage at the largest whole scale up to DVX_SCALE_MAX that fits
// the desktop, under a Game menu (Help, Exit). Closing the window by any of // the desktop, under a Game menu (Full Screen, Help, Exit). Closing the
// those routes ends the program at its next frame (dvxPortEndIfClosed). // window by any of those routes ends the program at its next frame
// (dvxPortEndIfClosed).
// //
// The window's content buffer is in the desktop's own pixel format (8, 15, // The window's content buffer is in the desktop's own pixel format (8, 15,
// 16 or 32 bits), so the present expands the stage's changed rows through // 16 or 32 bits), so the present expands the stage's changed rows through
@ -17,12 +19,31 @@
// or the user changing the video mode while the program runs, expands every // or the user changing the video mode while the program runs, expands every
// row. Runs of consecutive changed rows go to DVX as one rectangle each. // row. Runs of consecutive changed rows go to DVX as one rectangle each.
// //
// Full screen (Game > Full Screen, or Alt+Enter; the choice is kept in the
// program's settings folder, so it starts the way it was left): DVX's
// exclusive mode at 320x200 and 8 bits a pixel, which on DOS is VGA mode 13h
// or a VBE mode of that size. The model is the DOS port's: the screen's 256
// colours hold all 16 palettes (entry = palette * 16 + colour), so a pixel
// is the stage's palette index, (SCB << 4) | nibble, SCB palettes are exact,
// and a palette change is only the screen's palette: no pixel is expanded
// again. The same present writes the changed rows into the full-screen
// backbuffer (centred at the largest whole scale, should DVX give a larger
// mode or another depth, whose tables are then packed colours) and flushes
// each run to the screen. When the window loses the focus DVX gives the
// desktop back (Suspended): the present goes to the window again, the
// window redrawn first; with the focus back the screen is the program's
// again (Active), redrawn whole. At 8 bits and 1x the rows are expanded by
// the DOS port's own mode 13h expander (src/x86/x86Expand.h).
//
// Timing: a window has no vertical blank, so the frame rate is a virtual // Timing: a window has no vertical blank, so the frame rate is a virtual
// one, DVX_FRAME_HZ -- the DOS VGA rate, so programs, and the UBER timings, // one, DVX_FRAME_HZ -- the DOS VGA rate, so programs, and the UBER timings,
// behave as on DOS. One clock (DVX's platformMicros, from the program's first // behave as on DOS. One clock (DVX's platformMicros, from the program's first
// question) gives the milliseconds and the frame count, and jlWaitVBL waits // question) gives the milliseconds and the frame count, and jlWaitVBL waits
// for that clock's next frame boundary with gameFrameWait, giving the time to // for that clock's next frame boundary with gameFrameWait, giving the time to
// the other programs meanwhile. // the other programs meanwhile. On the whole screen jlWaitVBL gives the other
// programs one turn, then waits for the screen's real vertical retrace, as
// the DOS port does, so a present right after it does not tear; the frame
// count stays the same clock, so switching never makes it jump.
// //
// DVX multitasks cooperatively: the desktop -- the other programs, this // DVX multitasks cooperatively: the desktop -- the other programs, this
// program's window events (typed keys, the mouse, the close box) and the // program's window events (typed keys, the mouse, the close box) and the
@ -39,6 +60,7 @@
#include "port.h" #include "port.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
#include "x86Expand.h"
#include "dvxPort.h" #include "dvxPort.h"
// ----- Constants ----- // ----- Constants -----
@ -47,6 +69,9 @@
#define DVX_SCALE_MAX 3 #define DVX_SCALE_MAX 3
#define US_PER_MS 1000u #define US_PER_MS 1000u
// Full screen: 8 bits a pixel, the stage's 16 palettes in the screen's 256.
#define FULL_SCREEN_BPP 8
// $0RGB: 4 bits per channel, widened to 8 by x17 (0xF -> 0xFF). // $0RGB: 4 bits per channel, widened to 8 by x17 (0xF -> 0xFF).
#define RGB12_RED_SHIFT 8 #define RGB12_RED_SHIFT 8
#define RGB12_GREEN_SHIFT 4 #define RGB12_GREEN_SHIFT 4
@ -64,38 +89,87 @@
#define PALETTES_ALL 0xFFFFu #define PALETTES_ALL 0xFFFFu
// Game menu commands. // Game menu commands.
#define CMD_GAME_HELP 100 #define CMD_GAME_FULL_SCREEN 100
#define CMD_GAME_EXIT 101 #define CMD_GAME_HELP 101
#define CMD_GAME_EXIT 102
// The port's own settings, a file in the program's settings folder.
#define SETTINGS_FILE "JOEYLIB.INI"
#define SETTINGS_SECTION "display"
#define SETTINGS_FULL_SCREEN "fullScreen"
// ----- Types -----
// What the tables hold: the pixel for each palette entry.
typedef enum {
LUT_PACKED = 0, // the colour packed in the display's format (15 bits and up)
LUT_NEAREST, // the nearest colour of the 256-colour desktop's palette
LUT_INDEXED // the palette index itself: the whole screen at 8 bits, its palette the stage's
} LutKindE;
// Where a present goes: the window's content buffer, or while the program
// has the whole screen the full-screen backbuffer.
typedef struct {
uint8_t *pixels; // the stage's top-left pixel (NULL: nowhere to draw)
int32_t pitch; // bytes per row
int32_t bytesPerPixel;
int32_t scale; // pixels per stage pixel, each way
int32_t x; // the stage's top-left pixel in DVX's coordinates
int32_t y; // (the content area's, or the screen's)
int32_t rows; // the stage rows and columns that fit
int32_t cols;
bool full; // the whole screen
} PresentTargetT;
// ----- Prototypes ----- // ----- Prototypes -----
static uint64_t clockFrame(void); static uint64_t clockFrame(void);
static uint64_t clockMicros(void); static uint64_t clockMicros(void);
static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1); static void expandRow(const PresentTargetT *t, const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1);
static void fullSet(bool on);
static void lutBuild(const jlSurfaceT *src, uint16_t palettes); static void lutBuild(const jlSurfaceT *src, uint16_t palettes);
static LutKindE lutKind(const DisplayT *d);
static bool lutSync(const jlSurfaceT *src); static bool lutSync(const jlSurfaceT *src);
static void onClose(WindowT *win); static void onClose(WindowT *win);
static void onFullChange(WindowT *win, ExclusiveStateE state, void *userData);
static void onMenu(WindowT *win, int32_t menuId); static void onMenu(WindowT *win, int32_t menuId);
static void onPaint(WindowT *win, RectT *dirty); static void onPaint(WindowT *win, RectT *dirty);
static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidate); static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show);
static void redrawAll(void);
static bool settingFullScreenLoad(void);
static void settingFullScreenSave(bool on);
static const DisplayT *targetDisplay(void);
static bool targetGet(PresentTargetT *t);
static void targetShow(const PresentTargetT *t, int32_t x, int32_t y, int32_t w, int32_t h);
static bool windowCreate(AppContextT *ac); static bool windowCreate(AppContextT *ac);
// ----- Module state ----- // ----- Module state -----
WindowT *gDvxPortWindow = NULL; WindowT *gDvxPortWindow = NULL;
int32_t gDvxPortScale = 1;
static int32_t gScale = 1; // window pixels per stage pixel, each way
static GameFramePacerT gPacer; static GameFramePacerT gPacer;
static bool gClockStarted = false; static bool gClockStarted = false;
static bool gCloseRequested = false; static bool gCloseRequested = false;
static uint64_t gYieldFrame = 0; // the frame the program last yielded in static uint64_t gYieldFrame = 0; // the frame the program last yielded in
// Desktop pixel per palette entry, and what the tables were built for: the // Full screen: whether the program holds it (DVX's owner: the user's choice,
// desktop's bits per pixel (0 = not built) and, at 8 bits, its palette. // until the mode can no longer be set) and whether the screen is its own now
// (not suspended). DVX tells the port each change through onFullChange.
static bool gFullWanted = false;
static bool gFullActive = false;
// The pixel per palette entry, and what the tables were built for: their
// kind, the display's bits per pixel (0 = not built) and, for LUT_NEAREST,
// the desktop's palette.
static uint32_t gLut[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; static uint32_t gLut[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static LutKindE gLutKind = LUT_PACKED;
static int32_t gLutBits = 0; static int32_t gLutBits = 0;
static uint8_t gLutDesktopPalette[PAL_ENTRIES * PAL_CHANNELS]; static uint8_t gLutDesktopPalette[PAL_ENTRIES * PAL_CHANNELS];
// The full-screen palette (LUT_INDEXED): entry palette * 16 + colour.
static uint8_t gFullPalette[PAL_ENTRIES * PAL_CHANNELS];
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
// The virtual frame now running: frame n starts n / DVX_FRAME_HZ seconds // The virtual frame now running: frame n starts n / DVX_FRAME_HZ seconds
@ -116,36 +190,42 @@ static uint64_t clockMicros(void) {
} }
// Stage row y, pixels x0..x1-1 (both even), into the content buffer at the // Stage row y, pixels x0..x1-1 (both even), into the target at its scale:
// window's scale: each pixel repeated across, the row repeated down. // each pixel repeated across, the row repeated down. On the whole screen at
static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1) { // 8 bits and 1x the pixels are the DOS port's mode 13h bytes, so its
// expander does them (x0 and x1 are then whole chunky words, as it needs).
static void expandRow(const PresentTargetT *t, const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1) {
const uint32_t *lut; const uint32_t *lut;
const uint8_t *in; const uint8_t *in;
uint8_t *row; uint8_t *row;
int32_t scale;
int32_t bytesPerPixel;
int32_t pitch;
int32_t pairs; int32_t pairs;
int32_t i; int32_t i;
int32_t s; int32_t s;
if (gLutKind == LUT_INDEXED && t->scale == 1) {
x86ExpandRow(src, (int16_t)y, (int16_t)x0, (uint16_t)(x1 - x0), t->pixels + y * t->pitch);
return;
}
lut = gLut[src->scb[y]]; lut = gLut[src->scb[y]];
in = &src->pixels[y * SURFACE_BYTES_PER_ROW + x0 / STAGE_PIXELS_PER_BYTE]; in = &src->pixels[y * SURFACE_BYTES_PER_ROW + x0 / STAGE_PIXELS_PER_BYTE];
scale = gDvxPortScale; row = t->pixels + y * t->scale * t->pitch + x0 * t->scale * t->bytesPerPixel;
bytesPerPixel = dvxGetDisplay(gDvxPortProgram->shellCtx)->format.bytesPerPixel;
pitch = gDvxPortWindow->contentPitch;
row = gDvxPortWindow->contentBuf + y * scale * pitch + x0 * scale * bytesPerPixel;
pairs = (x1 - x0) / STAGE_PIXELS_PER_BYTE; pairs = (x1 - x0) / STAGE_PIXELS_PER_BYTE;
switch (bytesPerPixel) { switch (t->bytesPerPixel) {
case 1: { case 1: {
uint8_t *d = row; uint8_t *d = row;
uint8_t left;
uint8_t right;
for (i = 0; i < pairs; i++) { for (i = 0; i < pairs; i++) {
memset(d, (int)lut[in[i] >> NIBBLE_BITS], (size_t)scale); left = (uint8_t)lut[in[i] >> NIBBLE_BITS];
d += scale; right = (uint8_t)lut[in[i] & NIBBLE_MASK];
memset(d, (int)lut[in[i] & NIBBLE_MASK], (size_t)scale); for (s = 0; s < t->scale; s++) {
d += scale; *d++ = left;
}
for (s = 0; s < t->scale; s++) {
*d++ = right;
}
} }
break; break;
} }
@ -158,10 +238,10 @@ static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1)
for (i = 0; i < pairs; i++) { for (i = 0; i < pairs; i++) {
left = (uint16_t)lut[in[i] >> NIBBLE_BITS]; left = (uint16_t)lut[in[i] >> NIBBLE_BITS];
right = (uint16_t)lut[in[i] & NIBBLE_MASK]; right = (uint16_t)lut[in[i] & NIBBLE_MASK];
for (s = 0; s < scale; s++) { for (s = 0; s < t->scale; s++) {
*d++ = left; *d++ = left;
} }
for (s = 0; s < scale; s++) { for (s = 0; s < t->scale; s++) {
*d++ = right; *d++ = right;
} }
} }
@ -176,10 +256,10 @@ static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1)
for (i = 0; i < pairs; i++) { for (i = 0; i < pairs; i++) {
left = lut[in[i] >> NIBBLE_BITS]; left = lut[in[i] >> NIBBLE_BITS];
right = lut[in[i] & NIBBLE_MASK]; right = lut[in[i] & NIBBLE_MASK];
for (s = 0; s < scale; s++) { for (s = 0; s < t->scale; s++) {
*d++ = left; *d++ = left;
} }
for (s = 0; s < scale; s++) { for (s = 0; s < t->scale; s++) {
*d++ = right; *d++ = right;
} }
} }
@ -187,23 +267,48 @@ static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1)
} }
} }
for (s = 1; s < scale; s++) { for (s = 1; s < t->scale; s++) {
memcpy(row + s * pitch, row, (size_t)((x1 - x0) * scale * bytesPerPixel)); memcpy(row + s * t->pitch, row, (size_t)((x1 - x0) * t->scale * t->bytesPerPixel));
} }
} }
// Rebuild the tables of the palettes whose bits are set. // Full screen on or off; the Game menu's check mark follows. On: DVX sets
// the mode and tells onFullChange (Active) before this returns. Off: the
// desktop comes back, and the window is redrawn.
static void fullSet(bool on) {
AppContextT *ac;
ac = gDvxPortProgram->shellCtx;
if (on && dvxExclusiveBegin(ac, gDvxPortWindow, SURFACE_WIDTH, SURFACE_HEIGHT, FULL_SCREEN_BPP, onFullChange, NULL) != DVX_EXCL_OK) {
dvxLog("JoeyLib: %s: no full-screen mode", appDescriptor.name);
on = false;
}
if (!on && gFullWanted) {
dvxExclusiveEnd(ac, gDvxPortWindow);
gFullActive = false;
redrawAll();
}
gFullWanted = on;
wmMenuItemSetChecked(gDvxPortWindow->menuBar, CMD_GAME_FULL_SCREEN, on);
}
// Rebuild the tables of the palettes whose bits are set, for where the stage
// goes now; on the whole screen at 8 bits (LUT_INDEXED) their colours go to
// the screen's palette instead, and the tables are the indexes themselves.
static void lutBuild(const jlSurfaceT *src, uint16_t palettes) { static void lutBuild(const jlSurfaceT *src, uint16_t palettes) {
const DisplayT *d; const DisplayT *d;
uint8_t *entry;
uint16_t rgb; uint16_t rgb;
uint8_t r; uint8_t r;
uint8_t g; uint8_t g;
uint8_t b; uint8_t b;
uint8_t p; uint8_t p;
uint8_t c; uint8_t c;
uint8_t index;
d = dvxGetDisplay(gDvxPortProgram->shellCtx); d = targetDisplay();
for (p = 0; p < SURFACE_PALETTE_COUNT; p++) { for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
if ((palettes & (1u << p)) == 0) { if ((palettes & (1u << p)) == 0) {
continue; continue;
@ -213,25 +318,58 @@ static void lutBuild(const jlSurfaceT *src, uint16_t palettes) {
r = (uint8_t)(((rgb >> RGB12_RED_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); r = (uint8_t)(((rgb >> RGB12_RED_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
g = (uint8_t)(((rgb >> RGB12_GREEN_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); g = (uint8_t)(((rgb >> RGB12_GREEN_SHIFT) & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
b = (uint8_t)((rgb & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24); b = (uint8_t)((rgb & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
gLut[p][c] = d->palette != NULL ? gamePalNearest(d->palette, r, g, b) : packColor(d, r, g, b); index = (uint8_t)((p << NIBBLE_BITS) | c);
switch (gLutKind) {
case LUT_INDEXED:
entry = &gFullPalette[index * PAL_CHANNELS];
entry[0] = r;
entry[1] = g;
entry[2] = b;
gLut[p][c] = index;
break;
case LUT_NEAREST:
gLut[p][c] = gamePalNearest(d->palette, r, g, b);
break;
default:
gLut[p][c] = packColor(d, r, g, b);
break;
}
}
if (gLutKind == LUT_INDEXED) {
dvxExclusiveSetPalette(gDvxPortProgram->shellCtx, gFullPalette, p * SURFACE_COLORS_PER_PALETTE, SURFACE_COLORS_PER_PALETTE);
} }
} }
} }
// Rebuild every table when the desktop's pixel format, or its 256-colour // The kind of table display d takes. On the whole screen d->palette is the
// palette, is not the one they were built for (the user can change the // program's own palette (at any depth), not a desktop's to match.
// video mode while the program runs). True when it did: every row must be static LutKindE lutKind(const DisplayT *d) {
// expanded again. if (gFullActive) {
return d->format.bitsPerPixel == FULL_SCREEN_BPP ? LUT_INDEXED : LUT_PACKED;
}
return d->palette != NULL ? LUT_NEAREST : LUT_PACKED;
}
// Rebuild every table when they were not built for where the stage goes now
// (full screen came or went, or the user changed the video mode, or the
// 256-colour desktop's palette, while the program runs). True when it did:
// every row must be expanded again.
static bool lutSync(const jlSurfaceT *src) { static bool lutSync(const jlSurfaceT *src) {
const DisplayT *d; const DisplayT *d;
LutKindE kind;
d = dvxGetDisplay(gDvxPortProgram->shellCtx); d = targetDisplay();
if (d->format.bitsPerPixel == gLutBits && (d->palette == NULL || memcmp(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette)) == 0)) { kind = lutKind(d);
if (kind == gLutKind && d->format.bitsPerPixel == gLutBits && (kind != LUT_NEAREST || memcmp(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette)) == 0)) {
return false; return false;
} }
gLutKind = kind;
gLutBits = d->format.bitsPerPixel; gLutBits = d->format.bitsPerPixel;
if (d->palette != NULL) { if (kind == LUT_NEAREST) {
memcpy(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette)); memcpy(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette));
} }
lutBuild(src, PALETTES_ALL); lutBuild(src, PALETTES_ALL);
@ -245,9 +383,29 @@ static void onClose(WindowT *win) {
} }
// DVX's word on full screen: the screen is the program's (Active), the
// desktop is back while the window does not have the focus (Suspended), or
// the mode could not be set again and full screen is over (Ended). The
// stage is drawn again, whole, wherever it goes now.
static void onFullChange(WindowT *win, ExclusiveStateE state, void *userData) {
(void)win;
(void)userData;
gFullActive = state == ExclusiveActiveE;
if (state == ExclusiveEndedE) {
gFullWanted = false;
wmMenuItemSetChecked(gDvxPortWindow->menuBar, CMD_GAME_FULL_SCREEN, false);
}
redrawAll();
}
static void onMenu(WindowT *win, int32_t menuId) { static void onMenu(WindowT *win, int32_t menuId) {
(void)win; (void)win;
switch (menuId) { switch (menuId) {
case CMD_GAME_FULL_SCREEN:
dvxPortFullScreenToggle();
break;
case CMD_GAME_HELP: case CMD_GAME_HELP:
shellOpenHelp(gDvxPortProgram->shellCtx, gDvxPortProgram->helpFile); shellOpenHelp(gDvxPortProgram->shellCtx, gDvxPortProgram->helpFile);
break; break;
@ -264,10 +422,12 @@ static void onMenu(WindowT *win, int32_t menuId) {
// DVX wants the whole content buffer (the window's first showing, a video // DVX wants the whole content buffer (the window's first showing, a video
// mode change): expand the whole stage, which DVX then puts on the screen. // mode change): expand the whole stage, which DVX then puts on the screen.
// Not while the program has the whole screen: the window is not on show,
// and it is redrawn when the desktop comes back.
static void onPaint(WindowT *win, RectT *dirty) { static void onPaint(WindowT *win, RectT *dirty) {
(void)win; (void)win;
(void)dirty; (void)dirty;
if (gStage == NULL) { if (gStage == NULL || gFullActive) {
return; return;
} }
(void)lutSync(gStage); (void)lutSync(gStage);
@ -276,13 +436,10 @@ static void onPaint(WindowT *win, RectT *dirty) {
// Expand every row that is dirty, or whose palette bit is set in palettes, // Expand every row that is dirty, or whose palette bit is set in palettes,
// into the content buffer; with invalidate, tell DVX about each run of // into the target; with show, hand DVX each run of consecutive expanded rows
// consecutive expanded rows as one rectangle. // as one rectangle (targetShow).
static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidate) { static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) {
AppContextT *ac; PresentTargetT t;
int32_t scale;
int32_t rows;
int32_t cols;
int32_t y; int32_t y;
int32_t x0; int32_t x0;
int32_t x1; int32_t x1;
@ -291,27 +448,19 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidat
int32_t runX1; int32_t runX1;
uint8_t maxWord; uint8_t maxWord;
if (gDvxPortWindow == NULL || gDvxPortWindow->contentBuf == NULL) { if (!targetGet(&t) || t.pixels == NULL) {
return; return;
} }
ac = gDvxPortProgram->shellCtx;
scale = gDvxPortScale;
// Clip to the content area (it is the scaled stage unless DVX sized the
// window otherwise); whole pixel pairs only.
rows = gDvxPortWindow->contentH / scale;
cols = gDvxPortWindow->contentW / scale;
rows = rows < SURFACE_HEIGHT ? rows : SURFACE_HEIGHT;
cols = (cols < SURFACE_WIDTH ? cols : SURFACE_WIDTH) & ~(STAGE_PIXELS_PER_BYTE - 1);
runY0 = -1; runY0 = -1;
runX0 = 0; runX0 = 0;
runX1 = 0; runX1 = 0;
for (y = 0; y < rows; y++) { for (y = 0; y < t.rows; y++) {
if ((palettes & (1u << src->scb[y])) != 0) { if ((palettes & (1u << src->scb[y])) != 0) {
x0 = 0; x0 = 0;
x1 = SURFACE_WIDTH; x1 = SURFACE_WIDTH;
} else if (STAGE_DIRTY_ROW_CLEAN(y)) { } else if (STAGE_DIRTY_ROW_CLEAN(y)) {
if (runY0 >= 0 && invalidate) { if (runY0 >= 0 && show) {
dvxInvalidateRect(ac, gDvxPortWindow, runX0 * scale, runY0 * scale, (runX1 - runX0) * scale, (y - runY0) * scale); targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0);
} }
runY0 = -1; runY0 = -1;
continue; continue;
@ -321,11 +470,11 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidat
x0 = gStageMinWord[y] * STAGE_PIXELS_PER_WORD; x0 = gStageMinWord[y] * STAGE_PIXELS_PER_WORD;
x1 = (maxWord + 1) * STAGE_PIXELS_PER_WORD; x1 = (maxWord + 1) * STAGE_PIXELS_PER_WORD;
} }
x1 = x1 < cols ? x1 : cols; x1 = x1 < t.cols ? x1 : t.cols;
if (x0 >= x1) { if (x0 >= x1) {
continue; continue;
} }
expandRow(src, y, x0, x1); expandRow(&t, src, y, x0, x1);
gPresentCopiedBytes += (uint32_t)(x1 - x0); gPresentCopiedBytes += (uint32_t)(x1 - x0);
if (runY0 < 0) { if (runY0 < 0) {
runY0 = y; runY0 = y;
@ -336,13 +485,131 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidat
runX1 = x1 > runX1 ? x1 : runX1; runX1 = x1 > runX1 ? x1 : runX1;
} }
} }
if (runY0 >= 0 && invalidate) { if (runY0 >= 0 && show) {
dvxInvalidateRect(ac, gDvxPortWindow, runX0 * scale, runY0 * scale, (runX1 - runX0) * scale, (y - runY0) * scale); targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0);
} }
gPresentScannedRows += SURFACE_HEIGHT; gPresentScannedRows += SURFACE_HEIGHT;
} }
// Full screen came or went: new tables, and every row again, wherever the
// stage goes now. Before jlInit has the stage the first present does it.
static void redrawAll(void) {
gLutBits = 0;
if (gStage == NULL) {
return;
}
(void)lutSync(gStage);
presentRows(gStage, PALETTES_ALL, true);
}
// The saved choice: whether the program was on the whole screen when the
// user last switched.
static bool settingFullScreenLoad(void) {
char path[DVX_MAX_PATH];
PrefsHandleT *prefs;
bool on;
shellConfigPath(gDvxPortProgram, SETTINGS_FILE, path, (int32_t)sizeof(path));
prefs = prefsLoad(path);
if (prefs == NULL) {
return false;
}
on = prefsGetBool(prefs, SETTINGS_SECTION, SETTINGS_FULL_SCREEN, false);
prefsClose(prefs);
return on;
}
static void settingFullScreenSave(bool on) {
char path[DVX_MAX_PATH];
PrefsHandleT *prefs;
if (shellEnsureConfigDir(gDvxPortProgram) != 0) {
return;
}
shellConfigPath(gDvxPortProgram, SETTINGS_FILE, path, (int32_t)sizeof(path));
prefs = prefsLoad(path);
if (prefs == NULL) {
return;
}
prefsSetBool(prefs, SETTINGS_SECTION, SETTINGS_FULL_SCREEN, on);
if (!prefsSave(prefs)) {
dvxLog("JoeyLib: %s: could not save %s", appDescriptor.name, path);
}
prefsClose(prefs);
}
// The display the stage goes to now.
static const DisplayT *targetDisplay(void) {
AppContextT *ac;
ac = gDvxPortProgram->shellCtx;
return gFullActive ? dvxExclusiveDisplay(ac) : dvxGetDisplay(ac);
}
// Where the stage goes now. On the whole screen, centred at the largest
// whole scale that fits (as dvxExclusivePresent8 places a picture; DVX's
// mode is at least the stage's size); in the window, at the window's scale,
// clipped to the content area in whole pixel pairs. False without a window.
static bool targetGet(PresentTargetT *t) {
const DisplayT *d;
int32_t scaleW;
int32_t scaleH;
if (gDvxPortWindow == NULL) {
return false;
}
d = targetDisplay();
t->bytesPerPixel = d->format.bytesPerPixel;
t->full = gFullActive;
if (t->full) {
scaleW = d->width / SURFACE_WIDTH;
scaleH = d->height / SURFACE_HEIGHT;
t->scale = scaleW < scaleH ? scaleW : scaleH;
t->x = (d->width - SURFACE_WIDTH * t->scale) / 2;
t->y = (d->height - SURFACE_HEIGHT * t->scale) / 2;
t->pitch = d->pitch;
t->pixels = d->backBuf + t->y * t->pitch + t->x * t->bytesPerPixel;
t->rows = SURFACE_HEIGHT;
t->cols = SURFACE_WIDTH;
return true;
}
t->scale = gScale;
t->x = 0;
t->y = 0;
t->pitch = gDvxPortWindow->contentPitch;
t->pixels = gDvxPortWindow->contentBuf;
// The content area is the scaled stage unless DVX sized the window
// otherwise.
t->rows = gDvxPortWindow->contentH / t->scale;
t->cols = gDvxPortWindow->contentW / t->scale;
t->rows = t->rows < SURFACE_HEIGHT ? t->rows : SURFACE_HEIGHT;
t->cols = (t->cols < SURFACE_WIDTH ? t->cols : SURFACE_WIDTH) & ~(STAGE_PIXELS_PER_BYTE - 1);
return true;
}
// Stage rectangle (x, y, w, h) has changed in the target: flush it to the
// screen on the whole screen, or have DVX composite it from the window.
static void targetShow(const PresentTargetT *t, int32_t x, int32_t y, int32_t w, int32_t h) {
RectT r;
r.x = t->x + x * t->scale;
r.y = t->y + y * t->scale;
r.w = w * t->scale;
r.h = h * t->scale;
if (t->full) {
dvxExclusiveFlush(gDvxPortProgram->shellCtx, &r);
} else {
dvxInvalidateRect(gDvxPortProgram->shellCtx, gDvxPortWindow, r.x, r.y, r.w, r.h);
}
}
// The window: its content area the stage at the largest whole scale that // The window: its content area the stage at the largest whole scale that
// fits the desktop (up to DVX_SCALE_MAX), not resizable, under a Game menu. // fits the desktop (up to DVX_SCALE_MAX), not resizable, under a Game menu.
static bool windowCreate(AppContextT *ac) { static bool windowCreate(AppContextT *ac) {
@ -360,10 +627,10 @@ static bool windowCreate(AppContextT *ac) {
frameH = CHROME_TOTAL_TOP + CHROME_MENU_HEIGHT + CHROME_TOTAL_BOTTOM; frameH = CHROME_TOTAL_TOP + CHROME_MENU_HEIGHT + CHROME_TOTAL_BOTTOM;
scaleW = (ac->display.width - frameW) / SURFACE_WIDTH; scaleW = (ac->display.width - frameW) / SURFACE_WIDTH;
scaleH = (ac->display.height - frameH) / SURFACE_HEIGHT; scaleH = (ac->display.height - frameH) / SURFACE_HEIGHT;
gDvxPortScale = scaleW < scaleH ? scaleW : scaleH; gScale = scaleW < scaleH ? scaleW : scaleH;
gDvxPortScale = gDvxPortScale < 1 ? 1 : (gDvxPortScale > DVX_SCALE_MAX ? DVX_SCALE_MAX : gDvxPortScale); gScale = gScale < 1 ? 1 : (gScale > DVX_SCALE_MAX ? DVX_SCALE_MAX : gScale);
wantW = SURFACE_WIDTH * gDvxPortScale; wantW = SURFACE_WIDTH * gScale;
wantH = SURFACE_HEIGHT * gDvxPortScale; wantH = SURFACE_HEIGHT * gScale;
win = dvxCreateWindowCentered(ac, appDescriptor.name, wantW + frameW, wantH + frameH, false); win = dvxCreateWindowCentered(ac, appDescriptor.name, wantW + frameW, wantH + frameH, false);
if (win == NULL) { if (win == NULL) {
@ -379,6 +646,7 @@ static bool windowCreate(AppContextT *ac) {
bar = wmAddMenuBar(win); bar = wmAddMenuBar(win);
game = bar != NULL ? wmAddMenu(bar, "&Game") : NULL; game = bar != NULL ? wmAddMenu(bar, "&Game") : NULL;
if (game != NULL) { if (game != NULL) {
wmAddMenuCheckItem(game, "&Full Screen\tAlt+Enter", CMD_GAME_FULL_SCREEN, false);
wmAddMenuItem(game, "&Help", CMD_GAME_HELP); wmAddMenuItem(game, "&Help", CMD_GAME_HELP);
wmAddMenuSeparator(game); wmAddMenuSeparator(game);
wmAddMenuItem(game, "E&xit", CMD_GAME_EXIT); wmAddMenuItem(game, "E&xit", CMD_GAME_EXIT);
@ -390,7 +658,7 @@ static bool windowCreate(AppContextT *ac) {
dvxResizeWindow(ac, win, win->w + wantW - win->contentW, win->h + wantH - win->contentH); dvxResizeWindow(ac, win, win->w + wantW - win->contentW, win->h + wantH - win->contentH);
} }
gDvxPortWindow = win; gDvxPortWindow = win;
dvxLog("JoeyLib: %s: %" PRId32 "x%" PRId32 " at %" PRId32 "x on a %" PRId32 "x%" PRId32 " %" PRId32 "-bit desktop", appDescriptor.name, win->contentW, win->contentH, gDvxPortScale, ac->display.width, ac->display.height, ac->display.format.bitsPerPixel); dvxLog("JoeyLib: %s: %" PRId32 "x%" PRId32 " at %" PRId32 "x on a %" PRId32 "x%" PRId32 " %" PRId32 "-bit desktop", appDescriptor.name, win->contentW, win->contentH, gScale, ac->display.width, ac->display.height, ac->display.format.bitsPerPixel);
return true; return true;
} }
@ -404,6 +672,13 @@ void dvxPortEndIfClosed(void) {
} }
// The user's switch (Game > Full Screen, Alt+Enter), kept for the next run.
void dvxPortFullScreenToggle(void) {
fullSet(!gFullWanted);
settingFullScreenSave(gFullWanted);
}
void dvxPortService(void) { void dvxPortService(void) {
uint64_t frame; uint64_t frame;
@ -416,6 +691,20 @@ void dvxPortService(void) {
} }
// Content-area pixels in the window, screen pixels on the whole screen: the
// stage pixel under them, clamped to the stage.
void dvxPortStagePoint(int32_t *x, int32_t *y) {
PresentTargetT t;
if (targetGet(&t)) {
*x = (*x - t.x) / t.scale;
*y = (*y - t.y) / t.scale;
}
*x = *x < 0 ? 0 : (*x >= SURFACE_WIDTH ? SURFACE_WIDTH - 1 : *x);
*y = *y < 0 ? 0 : (*y >= SURFACE_HEIGHT ? SURFACE_HEIGHT - 1 : *y);
}
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
uint16_t jlpFrameCount(void) { uint16_t jlpFrameCount(void) {
@ -433,7 +722,13 @@ bool jlpInit(const jlConfigT *config) {
gCloseRequested = false; gCloseRequested = false;
gLutBits = 0; gLutBits = 0;
(void)clockMicros(); (void)clockMicros();
return windowCreate(gDvxPortProgram->shellCtx); if (!windowCreate(gDvxPortProgram->shellCtx)) {
return false;
}
if (settingFullScreenLoad()) {
fullSet(true);
}
return true;
} }
@ -453,7 +748,9 @@ void jlpPresent(const jlSurfaceT *src) {
palettes = PALETTES_ALL; palettes = PALETTES_ALL;
} else if (gStagePaletteDirty) { } else if (gStagePaletteDirty) {
lutBuild(src, gStagePaletteDirtyMask); lutBuild(src, gStagePaletteDirtyMask);
palettes = gStagePaletteDirtyMask; // On the whole screen at 8 bits the new colours are the screen's
// palette: no pixel changes.
palettes = gLutKind == LUT_INDEXED ? PALETTES_NONE : gStagePaletteDirtyMask;
} }
// An SCB change moves rows to other tables: expand them all again. // An SCB change moves rows to other tables: expand them all again.
if (gStageScbDirty) { if (gStageScbDirty) {
@ -467,21 +764,32 @@ void jlpPresent(const jlSurfaceT *src) {
} }
// Destroying the window gives the screen back too when the program has it
// (DVX tells onFullChange nothing then).
void jlpShutdown(void) { void jlpShutdown(void) {
if (gDvxPortWindow != NULL) { if (gDvxPortWindow != NULL) {
dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow); dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow);
gDvxPortWindow = NULL; gDvxPortWindow = NULL;
} }
gFullWanted = false;
gFullActive = false;
} }
// Block until the clock's next frame boundary, the other programs running // In the window: block until the clock's next frame boundary, the other
// meanwhile. Re-aiming the pacer at the frame now running makes every call // programs running meanwhile. Re-aiming the pacer at the frame now running
// wait for the NEXT boundary, as a vertical blank wait does, instead of the // makes every call wait for the NEXT boundary, as a vertical blank wait
// pacer's own catch-up rule. // does, instead of the pacer's own catch-up rule. On the whole screen: the
// other programs' turn, then the screen's real vertical retrace.
void jlpWaitVBL(void) { void jlpWaitVBL(void) {
if (gFullActive) {
tsYield();
dvxExclusiveWaitRetrace();
gYieldFrame = clockFrame();
} else {
gPacer.frame = clockFrame(); gPacer.frame = clockFrame();
(void)gameFrameWait(&gPacer); (void)gameFrameWait(&gPacer);
gYieldFrame = gPacer.frame; gYieldFrame = gPacer.frame;
}
dvxPortEndIfClosed(); dvxPortEndIfClosed();
} }

View file

@ -11,11 +11,12 @@
// keyboard layout already applied, and go straight into the core's queue: // keyboard layout already applied, and go straight into the core's queue:
// the callback runs in DVX's own task while the program is yielded, never // the callback runs in DVX's own task while the program is yielded, never
// in an interrupt, so nothing can race the program's jlInputGetChar. Ctrl // in an interrupt, so nothing can race the program's jlInputGetChar. Ctrl
// and Alt chords are commands, not text, as on DOS. // and Alt chords are commands, not text, as on DOS; Alt+Enter switches full
// screen on and off (hal.c).
// //
// Mouse: onMouse reports content-area pixels; they are divided by the scale // Mouse: onMouse reports content-area pixels (screen pixels on the whole
// and clamped to the stage, and published by the next poll, so the core's // screen); hal.c maps them to the stage pixel under them, and the next poll
// button edges compare two stable snapshots. // publishes it, so the core's button edges compare two stable snapshots.
// //
// Joysticks: DVX's joystick library, calibrated in DVX's Control Panel: // Joysticks: DVX's joystick library, calibrated in DVX's Control Panel:
// axes -32768..32767 scaled to -127..127, real analog where DOS reads the // axes -32768..32767 scaled to -127..127, real analog where DOS reads the
@ -43,6 +44,11 @@
// Modifiers that make a key a command rather than text. // Modifiers that make a key a command rather than text.
#define CHORD_MODIFIERS (KEY_MOD_CTRL | KEY_MOD_ALT) #define CHORD_MODIFIERS (KEY_MOD_CTRL | KEY_MOD_ALT)
// Alt+Enter comes as Enter or as the Enter key's scan code with Alt held,
// or (the keypad's Enter) as the BIOS code of Alt with the keypad Enter,
// which means Alt by itself: DVX may report no modifier with it.
#define SCAN_ALT_KEYPAD_ENTER 0xA6
// ----- Prototypes ----- // ----- Prototypes -----
static int8_t joyAxis(int32_t value); static int8_t joyAxis(int32_t value);
@ -129,6 +135,10 @@ static void joystickPoll(void) {
void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod) { void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod) {
(void)win; (void)win;
if (key == (SCAN_ALT_KEYPAD_ENTER | KEY_EXT_FLAG) || ((mod & KEY_MOD_ALT) != 0 && (key == KEY_ENTER || key == (KEY_SCAN_ENTER | KEY_EXT_FLAG)))) {
dvxPortFullScreenToggle();
return;
}
if ((mod & CHORD_MODIFIERS) != 0) { if ((mod & CHORD_MODIFIERS) != 0) {
return; return;
} }
@ -144,10 +154,9 @@ void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod) {
void dvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { void dvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
(void)win; (void)win;
x /= gDvxPortScale; dvxPortStagePoint(&x, &y);
y /= gDvxPortScale; gNextMouseX = (int16_t)x;
gNextMouseX = (int16_t)(x < 0 ? 0 : (x >= SURFACE_WIDTH ? SURFACE_WIDTH - 1 : x)); gNextMouseY = (int16_t)y;
gNextMouseY = (int16_t)(y < 0 ? 0 : (y >= SURFACE_HEIGHT ? SURFACE_HEIGHT - 1 : y));
gNextMouseButtons = buttons; gNextMouseButtons = buttons;
} }

View file

@ -5,10 +5,10 @@
// //
// DEFAULT, MODEM and PRINTER are COM1; SLOT is COM<unit> (1..4), as on DOS. // DEFAULT, MODEM and PRINTER are COM1; SLOT is COM<unit> (1..4), as on DOS.
// //
// One difference from every other port: DVX's library resets the modem // rs232Open raises DTR and RTS; rs232Close leaves them as they are, so
// lines when it closes a port, so jlSerialClose drops DTR there (a modem // jlSerialClose does not hang up a modem, and a port closed after
// hangs up). A program handing a live call back to another has to keep the // jlSerialSetDtr(false) stays down (scripts/verify-dvx-serial.sh checks
// port open. // both at the far end).
// //
// jlSerialPoll is also a point where the program comes back to the port // jlSerialPoll is also a point where the program comes back to the port
// every frame, so it yields to the desktop like the input poll does: a // every frame, so it yields to the desktop like the input poll does: a

80
src/x86/x86Expand.h Normal file
View file

@ -0,0 +1,80 @@
// x86 stage-row expansion to 8-bit palette indexes, shared by the DOS port
// (VGA mode 13h) and the DVX port (its full screen at 8 bits a pixel). Both
// load all 16 palettes x 16 colours into the screen's 256 entries, so a
// pixel is (SCB << 4) | nibble and no per-scanline palette programming is
// needed at display time. Each including file gets its own table.
#ifndef JOEY_X86_EXPAND_H
#define JOEY_X86_EXPAND_H
#include <stdbool.h>
#include <stdint.h>
#include "surfaceInternal.h"
// Expand LUT for x86ExpandRow. Maps a 4bpp-packed source byte to the
// 2-byte little-endian 8-bit output pair for the current scanline's
// SCB. The low byte (= dst[x], lower address = LEFT pixel) holds the
// packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT pixel)
// holds the LOW nibble. Storing lut[byte] as one 16-bit x86 write
// therefore lands the left pixel at the lower address, matching the
// old two-byte sequence exactly. Rebuilt only when the SCB-derived
// palBase changes from the previous row (most stages run long runs of
// equal SCB), so the per-row cost is one compare, not a 256-entry fill.
static uint16_t gX86ExpandLut[256];
static uint8_t gX86ExpandLutBase = 0;
static bool gX86ExpandLutValid = false;
static inline void x86ExpandLutBuild(uint8_t palBase) {
uint16_t byte;
uint8_t leftPixel;
uint8_t rightPixel;
for (byte = 0; byte < 256u; byte++) {
// Packed high nibble = LEFT pixel (lower x); packed low nibble
// = RIGHT pixel. The little-endian word puts the LEFT pixel in
// the low byte so a 16-bit store lands it at the lower (even)
// address -- byte-for-byte identical to the old pair store.
leftPixel = (uint8_t)(palBase | (byte >> 4));
rightPixel = (uint8_t)(palBase | (byte & 0x0Fu));
gX86ExpandLut[byte] = (uint16_t)((uint16_t)rightPixel << 8 | (uint16_t)leftPixel);
}
gX86ExpandLutBase = palBase;
gX86ExpandLutValid = true;
}
// Stage row y, pixels x..x+w-1, as palette indexes into dst[x..x+w-1]
// (dst is the row's first pixel).
static inline void x86ExpandRow(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) {
uint8_t palBase;
const uint8_t *srcBytes;
uint32_t *out;
uint16_t quads;
uint16_t i;
palBase = (uint8_t)(src->scb[y] << 4);
if (!gX86ExpandLutValid || palBase != gX86ExpandLutBase) {
x86ExpandLutBuild(palBase);
}
// Dirty bands are chunky-word aligned (both presents compute
// pixelX = minWord*4 and pixelW = words*4), so x and w are
// multiples of 4: &dst[x] is 32-bit aligned and the dword fusion
// below is tail-free by construction. Two source bytes (= 4
// nibbles) fuse into ONE aligned 32-bit store straight into the
// screen (perf-hunt-2 rank 6) -- little-endian puts the first LUT
// word in the low half, so the written bytes are identical to the
// old per-pair 16-bit stores. DOSBox's instruction-counting core
// charges the old loop ~3.5 instructions per VGA byte; halving the
// loop trips halves that.
srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
out = (uint32_t *)&dst[x];
quads = (uint16_t)(w >> 2);
for (i = 0; i < quads; i++) {
*out++ = (uint32_t)gX86ExpandLut[srcBytes[0]] | ((uint32_t)gX86ExpandLut[srcBytes[1]] << 16);
srcBytes += 2;
}
}
#endif