More DVX target work.
This commit is contained in:
parent
17343a0b4f
commit
c520b16179
29 changed files with 1269 additions and 366 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -63,6 +63,7 @@ nvram/
|
|||
# Crap I added
|
||||
stuff/*
|
||||
__pycache__/
|
||||
screenshots/
|
||||
|
||||
# Claude's working notes (design scratch, port write-ups, session state).
|
||||
# NOT the user manual -- that lives in docs/ and IS tracked.
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -8,8 +8,9 @@ platforms, and one DOS desktop, from a single codebase:
|
|||
- Atari ST (STF / 68000 baseline)
|
||||
- MS-DOS (386 / VGA, DJGPP)
|
||||
- Sharp X68000 (68000 @ 10 MHz, Human68k, elf2x68k)
|
||||
- DVX (the DOS desktop GUI in `../dvxgui`: each program a windowed `.app`,
|
||||
built with the DOS compiler against DVX's SDK)
|
||||
- DVX (the DOS desktop GUI in `../dvxgui`: each program a windowed `.app`
|
||||
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.
|
||||
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:
|
||||
`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,
|
||||
which copies DVX and its SDK from a built DVX tree
|
||||
(`./install.sh --dvx-dir PATH install dvx`; the default is `../dvxgui`); its
|
||||
examples land in `build/dvx/bin/apps/joeylib/<name>/`, laid out as they
|
||||
install in DVX.
|
||||
which unpacks DVX and its SDK from a DVX tree's distribution, `dist/dvx.zip`
|
||||
and `dist/sdk.zip` (`./install.sh --dvx-dir PATH install dvx`; the default
|
||||
is `../dvxgui`); its examples land in `build/dvx/bin/apps/joeylib/<name>/`,
|
||||
laid out as they install in DVX.
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -194,8 +194,9 @@ uint16_t jlFrameHz(void);
|
|||
|
||||
*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
|
||||
70 Hz. The real cadence may drift slightly; this is the figure to divide by
|
||||
when converting frames to seconds.
|
||||
70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the same
|
||||
frame count). The real cadence may drift slightly; this is the figure to
|
||||
divide by when converting frames to seconds.
|
||||
|
||||
=== jlMillisElapsed
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ bool jlMousePresent(void);
|
|||
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
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
whatever RTS was before the open. DVX is the exception: its serial library
|
||||
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.
|
||||
whatever RTS was before the open.
|
||||
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ firmware.
|
|||
|
||||
| `all`
|
||||
| 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.
|
||||
|===
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
| `--dvx-dir PATH`
|
||||
| The built DVX tree the `dvx` component copies DVX and its SDK from. The
|
||||
default is a `dvxgui` checkout beside this repository; the setting is
|
||||
remembered. The component only reads that tree: it copies what the builds
|
||||
and the emulator runs need into `toolchains/opt/dvx`, and rebuilding DVX
|
||||
there makes the component out of date, so `install dvx` copies it again.
|
||||
| The DVX tree the `dvx` component takes DVX and its SDK from: its
|
||||
distribution, `dist/dvx.zip` (DVX as it installs) and `dist/sdk.zip` (the
|
||||
SDK). The default is a `dvxgui` checkout beside this repository; the
|
||||
setting is remembered. The component only reads that tree: it unpacks both
|
||||
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/`:
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ them:
|
|||
|DVX
|
||||
|486 or later
|
||||
|what DVX needs
|
||||
|A program in a window on the DVX desktop, at 1x to 3x. The MS-DOS port's
|
||||
stage and code; a virtual 70 Hz. See <<_dvx_programs>>.
|
||||
|A program in a window on the DVX desktop, at 1x to 3x, or on the whole
|
||||
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
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ The API is uniform; these are the places where what you get differs.
|
|||
|60 Hz (NTSC)
|
||||
|70 Hz
|
||||
|60 Hz
|
||||
|70 Hz (virtual)
|
||||
|70 Hz (virtual; the real retrace on the whole screen)
|
||||
|
||||
|Pixel storage
|
||||
|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
|
||||
hashes are DOS's -- and the rest behaves like this:
|
||||
|
||||
* *A window, not the screen.* The window's content is the 320x200 stage at
|
||||
the largest whole scale, up to 3x, that fits the desktop: 1x at 640x480,
|
||||
2x at 800x600, 3x at 1024x768. The stage reaches the desktop through one
|
||||
colour table per palette, so SCB palettes are exact on 15-, 16- and 32-bit
|
||||
desktops. On a 256-colour desktop every colour is the nearest one in DVX's
|
||||
palette, which all programs share: gradients band.
|
||||
* *A window, or the whole screen.* The window's content is the 320x200 stage
|
||||
at the largest whole scale, up to 3x, that fits the desktop: 1x at
|
||||
640x480, 2x at 800x600, 3x at 1024x768. The stage reaches the desktop
|
||||
through one colour table per palette, so SCB palettes are exact on 15-,
|
||||
16- and 32-bit desktops. On a 256-colour desktop every colour is the
|
||||
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
|
||||
on DOS, and `jlWaitVBL()` waits for the next seventieth of a second of
|
||||
DVX's clock while the other programs run. Nothing prevents tearing.
|
||||
* *Keys DVX keeps.* While the window is active 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). The window's Game menu has Help
|
||||
(the program's help file) and Exit. Typed characters come through DVX's
|
||||
keyboard layout.
|
||||
DVX's clock while the other programs run; nothing prevents tearing. On the
|
||||
whole screen `jlWaitVBL()` gives the other programs a turn, then waits for
|
||||
the screen's real vertical retrace, as on DOS, so a present right after it
|
||||
does not tear. The frame count is the same clock in both, so switching
|
||||
never makes it jump.
|
||||
* *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
|
||||
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent` or
|
||||
`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.
|
||||
* *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes.
|
||||
DVX's keyboard joystick is not reported as one.
|
||||
* *Serial ports* belong to DVX's serial library, one program each. Closing a
|
||||
port there drops DTR, so a modem hangs up.
|
||||
* *Serial ports* belong to DVX's serial library, one program each. As on
|
||||
every port, closing one leaves DTR as it is.
|
||||
* A program's `main` is compiled as `jlGameMain`, which the port calls with
|
||||
`argv[0]` the `.app`'s path.
|
||||
|
||||
`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.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,65 @@
|
|||
// 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
|
||||
// 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>
|
||||
|
||||
#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) {
|
||||
jlSerialConfigT cfg;
|
||||
static const char banner[] = "JLSERTEST-READY\r\n";
|
||||
|
|
@ -22,12 +79,21 @@ int main(void) {
|
|||
for (;;) {
|
||||
uint8_t buf[64];
|
||||
uint16_t n;
|
||||
uint16_t i;
|
||||
|
||||
jlSerialPoll();
|
||||
n = jlSerialRead(buf, (uint16_t)sizeof(buf));
|
||||
if (n > 0u) {
|
||||
jlSerialWrite(buf, n);
|
||||
for (i = 0u; i < n; i++) {
|
||||
if (buf[i] == DTR_CHECK) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i > 0u) {
|
||||
jlSerialWrite(buf, i);
|
||||
}
|
||||
if (i < n) {
|
||||
dtrCheck(&cfg);
|
||||
}
|
||||
}
|
||||
/* not reached */
|
||||
// not reached
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@
|
|||
#define JOEYLIB_PLATFORM_NAME "Sharp X68000"
|
||||
#elif defined(JOEYLIB_PLATFORM_DVX)
|
||||
// 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_ENDIAN_LITTLE
|
||||
#define JOEYLIB_NATIVE_CHUNKY
|
||||
|
|
@ -470,13 +471,14 @@
|
|||
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
|
||||
#define JL_HAS_AUDIO_STOP_SFX
|
||||
#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,
|
||||
// through the jlpTileMapPaste alias) and the x86 sprite JIT. DVX's
|
||||
// libraries supply the services.
|
||||
#define JL_HAS_INIT // the program's window
|
||||
#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_SHUTDOWN
|
||||
#define JL_HAS_INPUT_POLL // held-key table, onKey characters, onMouse, joystick.lib
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
# toolchain:PORT emulator:PORT PORT toolchains emulators all
|
||||
# where PORT is iigs, amiga, atarist, dos, x68000 or dvx. `all`, `toolchains`
|
||||
# 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:
|
||||
# -y, --yes install missing host packages without asking
|
||||
|
|
@ -35,8 +35,9 @@
|
|||
# --keep-build keep the scratch build trees in cache/build
|
||||
# --llvm816-dir PATH use an existing llvm816 checkout for the IIgs
|
||||
# compiler (remembered; "--llvm816-dir clone" undoes it)
|
||||
# --dvx-dir PATH the built DVX tree the dvx component copies from
|
||||
# (remembered; default: dvxgui beside this repository)
|
||||
# --dvx-dir PATH the DVX tree the dvx component unpacks DVX and its SDK
|
||||
# from (its dist/dvx.zip and dist/sdk.zip; remembered;
|
||||
# default: dvxgui beside this repository)
|
||||
#
|
||||
# Examples:
|
||||
# ./install.sh install toolchains every cross compiler
|
||||
|
|
@ -422,7 +423,7 @@ while [ $# -gt 0 ]; do
|
|||
;;
|
||||
--dvx-dir)
|
||||
[ $# -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"
|
||||
(cd "$2" && pwd) > "$JOEY_STATE/dvxDir"
|
||||
shift
|
||||
|
|
|
|||
|
|
@ -1,56 +1,68 @@
|
|||
# DVX, the DOS desktop JoeyLib's dvx port runs in: its SDK (the headers,
|
||||
# sdk.mk and the host tools every DVX program is built with) and the runtime
|
||||
# tree the gates and joey-run boot (dvx.exe with its libraries, widgets,
|
||||
# programs, settings and system files). Copied from a built DVX tree, so the
|
||||
# builds and the emulator runs never touch the DVX project itself.
|
||||
# DVX, the DOS desktop JoeyLib's dvx port runs in: the runtime the gates and
|
||||
# joey-run boot (dvx.exe with its libraries, widgets, programs, settings and
|
||||
# system files) and its SDK (the headers, sdk.mk and the host tools every DVX
|
||||
# program is built with). Both are unpacked from a DVX tree's distribution:
|
||||
# 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
|
||||
# a dvxgui checkout beside this repository. Rebuilding DVX there makes this
|
||||
# component out of date, and `install dvx` copies it again.
|
||||
# a dvxgui checkout beside this repository. The pin is the zips' checksums, so
|
||||
# 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
|
||||
# it out; `install dvx` (or toolchain:dvx) asks for it.
|
||||
compGroups="toolchain:dvx optional"
|
||||
compRequires="host djgpp"
|
||||
compHostDeps=""
|
||||
compHostDeps="unzip"
|
||||
|
||||
dvxDir="$(cat "$JOEY_STATE/dvxDir" 2>/dev/null || true)"
|
||||
if [ -z "$dvxDir" ]; then
|
||||
dvxDir="$(cd "$JOEY_REPO/.." && pwd)/dvxgui"
|
||||
fi
|
||||
dvxBin="$dvxDir/bin"
|
||||
# What the runtime needs from bin/: not the host test tools (bin/host),
|
||||
# the test modules, the link map, the log or the installer.
|
||||
dvxRuntime="dvx.exe config system libs widgets apps"
|
||||
dvxRuntimeZip="$dvxDir/dist/dvx.zip"
|
||||
dvxSdkZip="$dvxDir/dist/sdk.zip"
|
||||
# The folder sdk.zip holds the SDK in.
|
||||
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"
|
||||
|
||||
|
||||
# dvxListing -- every file copied, with its size and time: the pin, so a
|
||||
# rebuilt DVX makes this component out of date.
|
||||
dvxListing() {
|
||||
local part
|
||||
[ -d "$dvxBin" ] || return 0
|
||||
for part in $dvxRuntime $dvxSdk; do
|
||||
[ -e "$dvxBin/$part" ] || continue
|
||||
find "$dvxBin/$part" -type f -printf '%P %s %T@\n'
|
||||
done | sort
|
||||
# dvxSdkPin -- what the SDK comes from: sdk.zip's checksum, or without it
|
||||
# every file of bin/sdk with its size and time.
|
||||
dvxSdkPin() {
|
||||
if [ -f "$dvxSdkZip" ]; then
|
||||
printf 'zip:%s\n' "$(sha256Of "$dvxSdkZip")"
|
||||
elif [ -d "$dvxSdkTree" ]; then
|
||||
printf 'tree:%s\n' "$(find "$dvxSdkTree" -type f -printf '%P %s %T@\n' | sort | sha1sum | cut -d' ' -f1)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
compPin="dir:$dvxDir:$(dvxListing | sha1sum | cut -d' ' -f1)"
|
||||
compPin="dir:$dvxDir:dvx:$([ -f "$dvxRuntimeZip" ] && sha256Of "$dvxRuntimeZip"):sdk:$(dvxSdkPin)"
|
||||
|
||||
|
||||
compInstall() {
|
||||
local part
|
||||
[ -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 "$dvxBin/$dvxSdk/sdk.mk" ] || die "$dvxDir has no SDK (bin/sdk/sdk.mk); build DVX there first"
|
||||
local tmp
|
||||
[ -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"
|
||||
rm -rf "$compDir"
|
||||
mkdir -p "$compDir"
|
||||
for part in $dvxRuntime $dvxSdk; do
|
||||
[ -e "$dvxBin/$part" ] || die "$dvxBin/$part is missing; is the DVX build complete?"
|
||||
cp -a "$dvxBin/$part" "$compDir/"
|
||||
done
|
||||
extractArchive "$dvxRuntimeZip" "$compDir"
|
||||
if [ -f "$dvxSdkZip" ]; then
|
||||
tmp="$(freshBuildDir "$compId")"
|
||||
extractArchive "$dvxSdkZip" "$tmp"
|
||||
[ -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:
|
||||
# proves the SDK, its host tools and the compiler work together.
|
||||
# Build a one-window DVX program with the SDK and JoeyLib's DJGPP: proves the
|
||||
# SDK, its host tools and the compiler work together.
|
||||
compVerify() {
|
||||
local tmp
|
||||
local status=1
|
||||
|
|
|
|||
12
joey-run
12
joey-run
|
|
@ -48,11 +48,13 @@
|
|||
# unit 0) and DOS's (and DVX's) COM1.
|
||||
#
|
||||
# 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
|
||||
# program is DVX's desktop program, in its window on a 1024x768 desktop
|
||||
# (640x480 with --headless). Interactive runs keep that DVX in
|
||||
# build/dvx/run, so what the program saves (in DVX's CONFIG folder) is
|
||||
# still there next time. The link is raw TCP, without telnet negotiation.
|
||||
# unpacked from a DVX tree's dist/dvx.zip (install dvx), which boots
|
||||
# straight into it: the program is DVX's desktop program, in its window on a
|
||||
# 1024x768 desktop (640x480 with --headless). Game > Full Screen, or Alt
|
||||
# with the keypad's Enter (DOSBox keeps Alt+Enter for its own full screen),
|
||||
# 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
|
||||
# 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
|
||||
|
|
|
|||
13
make/dvx.mk
13
make/dvx.mk
|
|
@ -1,10 +1,11 @@
|
|||
# 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
|
||||
# library, libjoey.a, is built here with JoeyLib's own DJGPP (DVX is built
|
||||
# with the same gcc 12.2.0) against the SDK the installer copied from a DVX
|
||||
# tree into toolchains/opt/dvx (DVX_SDK); the stage and the graphics are the
|
||||
# DOS port's (src/x86, the chunky generics). Each example is then a DVX SDK
|
||||
# A JoeyLib program on DVX runs in a window of the DVX desktop, or on the
|
||||
# whole screen (src/dvx). The library, libjoey.a, is built here with
|
||||
# JoeyLib's own DJGPP (DVX is built with the same gcc 12.2.0) against the SDK
|
||||
# the installer unpacked from a DVX tree's dist/sdk.zip into
|
||||
# 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
|
||||
# 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,
|
||||
|
|
@ -207,6 +208,8 @@ app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data)
|
|||
'$$($(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.' \
|
||||
'' \
|
||||
'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
|
||||
@printf '%s\n' \
|
||||
'name text "$$($(1)_TITLE)"' \
|
||||
|
|
|
|||
|
|
@ -13,8 +13,14 @@
|
|||
# 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.
|
||||
#
|
||||
# scripts/bench-dvx.sh # default 600 s timeout
|
||||
# TIMEOUT=900 scripts/bench-dvx.sh # override
|
||||
# --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 --full-screen # on the whole screen
|
||||
# TIMEOUT=900 scripts/bench-dvx.sh # override
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -24,6 +30,13 @@ source "$repo/scripts/lib/dvxGate.sh"
|
|||
joeyNeed DOSBOX dosbox
|
||||
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}
|
||||
# The emulator's hard cap, a little past the wait, whatever this script does.
|
||||
wallSeconds=$((timeoutSeconds + 30))
|
||||
|
|
@ -44,6 +57,9 @@ cleanup() {
|
|||
trap cleanup EXIT
|
||||
|
||||
dvxStage "$work/c" "$(dvxAppDir uber)"
|
||||
if [ "$fullScreen" = 1 ]; then
|
||||
dvxFullScreen "$work/c" uber
|
||||
fi
|
||||
dvxDosboxConf "$work/dosbox.conf" "$work/c"
|
||||
mkdir -p "$work/xdg"
|
||||
rm -f "$out"
|
||||
|
|
@ -76,6 +92,11 @@ joeyStop "$dosbox" "$guard" 2> /dev/null
|
|||
dosbox=""
|
||||
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)
|
||||
if ! grep -q "$doneLine" "$out"; then
|
||||
echo "bench-dvx: WARNING -- timed out before the completion line (partial capture)" >&2
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# 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
|
||||
# runtime the installer took from a DVX tree (toolchains/opt/dvx, without
|
||||
# its SDK) plus JoeyLib programs from build/dvx/bin -- whose DVX.INI makes
|
||||
# runtime the installer unpacked from DVX's dist/dvx.zip (toolchains/opt/dvx,
|
||||
# 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
|
||||
# Staging run of that folder as drive C:. Nothing here writes to the DVX
|
||||
# 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.
|
||||
DVX_VENDOR=joeylib
|
||||
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).
|
||||
DVX_RUNTIME="dvx.exe config system libs widgets apps"
|
||||
# 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,
|
||||
# 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.
|
||||
|
|
@ -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
|
||||
# place a program that failed to load is explained.
|
||||
dvxDvxLog() {
|
||||
|
|
|
|||
98
scripts/lib/keysRow.py
Executable file
98
scripts/lib/keysRow.py
Executable 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))
|
||||
|
|
@ -13,6 +13,17 @@ works too).
|
|||
|
||||
WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP 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
|
||||
|
|
@ -30,6 +41,59 @@ CONNECT_RETRY_SECONDS = 0.5
|
|||
READ_SIZE = 256
|
||||
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):
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
|
@ -64,8 +128,57 @@ def openPty(pathFile):
|
|||
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):
|
||||
# send(bytes); receive() -> bytes, b"" for "nothing yet", None at EOF.
|
||||
# Returns the exit status and the stream.
|
||||
got = b""
|
||||
sentAt = None
|
||||
lastSend = 0.0
|
||||
|
|
@ -81,13 +194,15 @@ def loop(send, receive, wall, start, banner):
|
|||
got += data
|
||||
if sentAt is not None and PROBE in got[sentAt:]:
|
||||
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])
|
||||
return 1
|
||||
return 1, got
|
||||
|
||||
|
||||
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__)
|
||||
return 2
|
||||
mode = argv[1]
|
||||
|
|
@ -107,7 +222,7 @@ def main(argv):
|
|||
except OSError:
|
||||
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])
|
||||
conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start)
|
||||
|
|
@ -116,6 +231,8 @@ def main(argv):
|
|||
return 1
|
||||
conn.settimeout(POLL_SECONDS)
|
||||
|
||||
lines = LineDecoder()
|
||||
|
||||
def receive():
|
||||
try:
|
||||
data = conn.recv(READ_SIZE)
|
||||
|
|
@ -123,9 +240,14 @@ def main(argv):
|
|||
return b""
|
||||
except OSError:
|
||||
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__":
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
# tests/goldens/uber (x68000 against the IIgs's, its own gate;
|
||||
# dvx against the DOS port's, whose stage it shows)
|
||||
# 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
|
||||
# of the host fixture both ways, pixel-exact and with the host
|
||||
# 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 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 dvx full scripts/verify-dvx-fullscreen.sh
|
||||
tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1
|
||||
tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh
|
||||
tilebank atarist tilebank scripts/verify-atarist-tilebank.sh
|
||||
|
|
|
|||
226
scripts/verify-dvx-fullscreen.sh
Executable file
226
scripts/verify-dvx-fullscreen.sh
Executable 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)"
|
||||
|
|
@ -8,7 +8,8 @@
|
|||
# verification scanline (row 198: sentinel, count, every received
|
||||
# character as two pixel nibbles) somewhere -- the window's content,
|
||||
# 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
|
||||
# as hex to its joeylog.txt (in its settings folder), which must equal
|
||||
# the typed ASCII.
|
||||
|
|
@ -105,73 +106,11 @@ if [ -z "$log" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$png" "$target" "$work/joeylog.txt" <<'PY'
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
png, target, logPath = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
fails = []
|
||||
|
||||
# KEYS palette 0 (buildPalette): a gray ramp with four demo colours and red
|
||||
# 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
|
||||
fails=""
|
||||
python3 "$repo/scripts/lib/keysRow.py" "$png" "$target" || fails="the present"
|
||||
dvxKeysTyped "$work/joeylog.txt" "$target" || fails="${fails:+$fails, }the log"
|
||||
if [ -n "$fails" ]; then
|
||||
echo "verify-dvx-input: FAIL - $fails"
|
||||
exit 1
|
||||
fi
|
||||
echo "verify-dvx-input: PASS (typed \"$target\": all ${#target} chars reached jlInputGetChar, and the present shows them)"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@
|
|||
# serial layer -> the DVX port (src/dvx/serial.c) -> DVX's rs232 library and
|
||||
# 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
|
||||
# Env: DVX_SERIAL_PORT TCP port the peer listens on (default 6803)
|
||||
# Requires: DOSBox Staging, Xvfb, python3, the dvx component; the sertest
|
||||
|
|
@ -22,8 +29,8 @@ joeyNeed DVX_ROOT dvx
|
|||
|
||||
program=sertest
|
||||
port="${DVX_SERIAL_PORT:-6803}"
|
||||
peerWall=90 # DVX boot, then the banner and the echo
|
||||
runSeconds=100 # SERTEST echoes forever: DOSBox ends by timeout
|
||||
peerWall=95 # DVX boot, the banner and the echo, then the DTR check
|
||||
runSeconds=110 # SERTEST idles forever after the check: DOSBox ends by timeout
|
||||
|
||||
dvxNeedBuild "$program"
|
||||
|
||||
|
|
@ -41,10 +48,10 @@ trap cleanup EXIT
|
|||
|
||||
dvxStage "$work/c" "$(dvxAppDir "$program")"
|
||||
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"
|
||||
|
||||
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=$!
|
||||
sleep 1
|
||||
|
||||
|
|
@ -67,7 +74,7 @@ dosbox=""
|
|||
guard=""
|
||||
cat "$work/peer.log"
|
||||
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
|
||||
echo "verify-dvx-serial: FAIL (DVX.LOG follows)" >&2
|
||||
dvxDvxLog "$work/c" >&2
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
#include "port.h"
|
||||
#include "surfaceInternal.h"
|
||||
#include "x86Expand.h"
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
|
|
@ -39,9 +40,7 @@
|
|||
// ----- Prototypes -----
|
||||
|
||||
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 rebuildExpandLut(uint8_t palBase);
|
||||
static void uploadPalette(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
|
||||
// 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) -----
|
||||
|
||||
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) {
|
||||
uint16_t p;
|
||||
uint16_t c;
|
||||
|
|
@ -287,7 +222,7 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
|
||||
// Walk per-row dirty bands: each chunky word holds 4 mode-13h
|
||||
// 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
|
||||
// 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
|
||||
|
|
@ -324,7 +259,7 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
}
|
||||
pixelX = (int16_t)((uint16_t)minWord << 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
// 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
|
||||
// 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
|
||||
// joysticks; audio.c, save.c and serial.c the add-on services. Everything
|
||||
// here is named dvxPort* so nothing collides with DVX's own dvx* names.
|
||||
// scale) and full screen, the frame clock and the present; input.c the
|
||||
// keyboard, mouse and joysticks; audio.c, save.c and serial.c the add-on
|
||||
// services. Everything here is named dvxPort* so nothing collides with
|
||||
// DVX's own dvx* names.
|
||||
#ifndef JOEY_DVX_PORT_H
|
||||
#define JOEY_DVX_PORT_H
|
||||
|
||||
|
|
@ -42,17 +43,25 @@ int32_t dvxPortRun(DxeAppContextT *ctx);
|
|||
// ----- hal.c -----
|
||||
|
||||
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,
|
||||
// Alt+F4, Game > Exit).
|
||||
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
|
||||
// input poll, the present): yield to the desktop if the program has not
|
||||
// this virtual frame, then dvxPortEndIfClosed.
|
||||
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 -----
|
||||
|
||||
void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include "dvxApp.h"
|
||||
#include "dvxPal.h"
|
||||
#include "dvxPlat.h"
|
||||
#include "dvxPrefs.h"
|
||||
#include "dvxVideo.h"
|
||||
#include "dvxWm.h"
|
||||
#include "shellApp.h"
|
||||
|
|
|
|||
522
src/dvx/hal.c
522
src/dvx/hal.c
|
|
@ -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
|
||||
// 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 desktop, under a Game menu (Help, Exit). Closing the window by any of
|
||||
// those routes ends the program at its next frame (dvxPortEndIfClosed).
|
||||
// the desktop, under a Game menu (Full Screen, Help, Exit). Closing the
|
||||
// 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,
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// question) gives the milliseconds and the frame count, and jlWaitVBL waits
|
||||
// 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
|
||||
// program's window events (typed keys, the mouse, the close box) and the
|
||||
|
|
@ -39,6 +60,7 @@
|
|||
|
||||
#include "port.h"
|
||||
#include "surfaceInternal.h"
|
||||
#include "x86Expand.h"
|
||||
#include "dvxPort.h"
|
||||
|
||||
// ----- Constants -----
|
||||
|
|
@ -47,6 +69,9 @@
|
|||
#define DVX_SCALE_MAX 3
|
||||
#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).
|
||||
#define RGB12_RED_SHIFT 8
|
||||
#define RGB12_GREEN_SHIFT 4
|
||||
|
|
@ -64,38 +89,87 @@
|
|||
#define PALETTES_ALL 0xFFFFu
|
||||
|
||||
// Game menu commands.
|
||||
#define CMD_GAME_HELP 100
|
||||
#define CMD_GAME_EXIT 101
|
||||
#define CMD_GAME_FULL_SCREEN 100
|
||||
#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 -----
|
||||
|
||||
static uint64_t clockFrame(void);
|
||||
static uint64_t clockMicros(void);
|
||||
static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1);
|
||||
static void lutBuild(const jlSurfaceT *src, uint16_t palettes);
|
||||
static bool lutSync(const jlSurfaceT *src);
|
||||
static void onClose(WindowT *win);
|
||||
static void onMenu(WindowT *win, int32_t menuId);
|
||||
static void onPaint(WindowT *win, RectT *dirty);
|
||||
static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidate);
|
||||
static bool windowCreate(AppContextT *ac);
|
||||
static uint64_t clockFrame(void);
|
||||
static uint64_t clockMicros(void);
|
||||
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 LutKindE lutKind(const DisplayT *d);
|
||||
static bool lutSync(const jlSurfaceT *src);
|
||||
static void onClose(WindowT *win);
|
||||
static void onFullChange(WindowT *win, ExclusiveStateE state, void *userData);
|
||||
static void onMenu(WindowT *win, int32_t menuId);
|
||||
static void onPaint(WindowT *win, RectT *dirty);
|
||||
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);
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
WindowT *gDvxPortWindow = NULL;
|
||||
int32_t gDvxPortScale = 1;
|
||||
|
||||
static int32_t gScale = 1; // window pixels per stage pixel, each way
|
||||
static GameFramePacerT gPacer;
|
||||
static bool gClockStarted = 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
|
||||
// desktop's bits per pixel (0 = not built) and, at 8 bits, its palette.
|
||||
// Full screen: whether the program holds it (DVX's owner: the user's choice,
|
||||
// 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 LutKindE gLutKind = LUT_PACKED;
|
||||
static int32_t gLutBits = 0;
|
||||
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) -----
|
||||
|
||||
// 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
|
||||
// window's scale: each pixel repeated across, the row repeated down.
|
||||
static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1) {
|
||||
// Stage row y, pixels x0..x1-1 (both even), into the target at its scale:
|
||||
// each pixel repeated across, the row repeated down. On the whole screen at
|
||||
// 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 uint8_t *in;
|
||||
uint8_t *row;
|
||||
int32_t scale;
|
||||
int32_t bytesPerPixel;
|
||||
int32_t pitch;
|
||||
int32_t pairs;
|
||||
int32_t i;
|
||||
int32_t s;
|
||||
|
||||
lut = gLut[src->scb[y]];
|
||||
in = &src->pixels[y * SURFACE_BYTES_PER_ROW + x0 / STAGE_PIXELS_PER_BYTE];
|
||||
scale = gDvxPortScale;
|
||||
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;
|
||||
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]];
|
||||
in = &src->pixels[y * SURFACE_BYTES_PER_ROW + x0 / STAGE_PIXELS_PER_BYTE];
|
||||
row = t->pixels + y * t->scale * t->pitch + x0 * t->scale * t->bytesPerPixel;
|
||||
pairs = (x1 - x0) / STAGE_PIXELS_PER_BYTE;
|
||||
|
||||
switch (bytesPerPixel) {
|
||||
switch (t->bytesPerPixel) {
|
||||
case 1: {
|
||||
uint8_t *d = row;
|
||||
uint8_t left;
|
||||
uint8_t right;
|
||||
|
||||
for (i = 0; i < pairs; i++) {
|
||||
memset(d, (int)lut[in[i] >> NIBBLE_BITS], (size_t)scale);
|
||||
d += scale;
|
||||
memset(d, (int)lut[in[i] & NIBBLE_MASK], (size_t)scale);
|
||||
d += scale;
|
||||
left = (uint8_t)lut[in[i] >> NIBBLE_BITS];
|
||||
right = (uint8_t)lut[in[i] & NIBBLE_MASK];
|
||||
for (s = 0; s < t->scale; s++) {
|
||||
*d++ = left;
|
||||
}
|
||||
for (s = 0; s < t->scale; s++) {
|
||||
*d++ = right;
|
||||
}
|
||||
}
|
||||
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++) {
|
||||
left = (uint16_t)lut[in[i] >> NIBBLE_BITS];
|
||||
right = (uint16_t)lut[in[i] & NIBBLE_MASK];
|
||||
for (s = 0; s < scale; s++) {
|
||||
for (s = 0; s < t->scale; s++) {
|
||||
*d++ = left;
|
||||
}
|
||||
for (s = 0; s < scale; s++) {
|
||||
for (s = 0; s < t->scale; s++) {
|
||||
*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++) {
|
||||
left = lut[in[i] >> NIBBLE_BITS];
|
||||
right = lut[in[i] & NIBBLE_MASK];
|
||||
for (s = 0; s < scale; s++) {
|
||||
for (s = 0; s < t->scale; s++) {
|
||||
*d++ = left;
|
||||
}
|
||||
for (s = 0; s < scale; s++) {
|
||||
for (s = 0; s < t->scale; s++) {
|
||||
*d++ = right;
|
||||
}
|
||||
}
|
||||
|
|
@ -187,51 +267,109 @@ static void expandRow(const jlSurfaceT *src, int32_t y, int32_t x0, int32_t x1)
|
|||
}
|
||||
}
|
||||
|
||||
for (s = 1; s < scale; s++) {
|
||||
memcpy(row + s * pitch, row, (size_t)((x1 - x0) * scale * bytesPerPixel));
|
||||
for (s = 1; s < t->scale; s++) {
|
||||
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) {
|
||||
const DisplayT *d;
|
||||
uint8_t *entry;
|
||||
uint16_t rgb;
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
uint8_t p;
|
||||
uint8_t c;
|
||||
uint8_t index;
|
||||
|
||||
d = dvxGetDisplay(gDvxPortProgram->shellCtx);
|
||||
d = targetDisplay();
|
||||
for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
|
||||
if ((palettes & (1u << p)) == 0) {
|
||||
continue;
|
||||
}
|
||||
for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
|
||||
rgb = src->palette[p][c];
|
||||
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);
|
||||
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);
|
||||
rgb = src->palette[p][c];
|
||||
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);
|
||||
b = (uint8_t)((rgb & RGB12_CHANNEL_MASK) * RGB12_TO_RGB24);
|
||||
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
|
||||
// palette, is not the one they were built for (the user can change the
|
||||
// video mode while the program runs). True when it did: every row must be
|
||||
// expanded again.
|
||||
// The kind of table display d takes. On the whole screen d->palette is the
|
||||
// program's own palette (at any depth), not a desktop's to match.
|
||||
static LutKindE lutKind(const DisplayT *d) {
|
||||
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) {
|
||||
const DisplayT *d;
|
||||
LutKindE kind;
|
||||
|
||||
d = dvxGetDisplay(gDvxPortProgram->shellCtx);
|
||||
if (d->format.bitsPerPixel == gLutBits && (d->palette == NULL || memcmp(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette)) == 0)) {
|
||||
d = targetDisplay();
|
||||
kind = lutKind(d);
|
||||
if (kind == gLutKind && d->format.bitsPerPixel == gLutBits && (kind != LUT_NEAREST || memcmp(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette)) == 0)) {
|
||||
return false;
|
||||
}
|
||||
gLutKind = kind;
|
||||
gLutBits = d->format.bitsPerPixel;
|
||||
if (d->palette != NULL) {
|
||||
if (kind == LUT_NEAREST) {
|
||||
memcpy(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette));
|
||||
}
|
||||
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) {
|
||||
(void)win;
|
||||
switch (menuId) {
|
||||
case CMD_GAME_FULL_SCREEN:
|
||||
dvxPortFullScreenToggle();
|
||||
break;
|
||||
|
||||
case CMD_GAME_HELP:
|
||||
shellOpenHelp(gDvxPortProgram->shellCtx, gDvxPortProgram->helpFile);
|
||||
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
|
||||
// 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) {
|
||||
(void)win;
|
||||
(void)dirty;
|
||||
if (gStage == NULL) {
|
||||
if (gStage == NULL || gFullActive) {
|
||||
return;
|
||||
}
|
||||
(void)lutSync(gStage);
|
||||
|
|
@ -276,42 +436,31 @@ static void onPaint(WindowT *win, RectT *dirty) {
|
|||
|
||||
|
||||
// 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
|
||||
// consecutive expanded rows as one rectangle.
|
||||
static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidate) {
|
||||
AppContextT *ac;
|
||||
int32_t scale;
|
||||
int32_t rows;
|
||||
int32_t cols;
|
||||
int32_t y;
|
||||
int32_t x0;
|
||||
int32_t x1;
|
||||
int32_t runY0;
|
||||
int32_t runX0;
|
||||
int32_t runX1;
|
||||
uint8_t maxWord;
|
||||
// into the target; with show, hand DVX each run of consecutive expanded rows
|
||||
// as one rectangle (targetShow).
|
||||
static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool show) {
|
||||
PresentTargetT t;
|
||||
int32_t y;
|
||||
int32_t x0;
|
||||
int32_t x1;
|
||||
int32_t runY0;
|
||||
int32_t runX0;
|
||||
int32_t runX1;
|
||||
uint8_t maxWord;
|
||||
|
||||
if (gDvxPortWindow == NULL || gDvxPortWindow->contentBuf == NULL) {
|
||||
if (!targetGet(&t) || t.pixels == NULL) {
|
||||
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;
|
||||
runX0 = 0;
|
||||
runX1 = 0;
|
||||
for (y = 0; y < rows; y++) {
|
||||
for (y = 0; y < t.rows; y++) {
|
||||
if ((palettes & (1u << src->scb[y])) != 0) {
|
||||
x0 = 0;
|
||||
x1 = SURFACE_WIDTH;
|
||||
} else if (STAGE_DIRTY_ROW_CLEAN(y)) {
|
||||
if (runY0 >= 0 && invalidate) {
|
||||
dvxInvalidateRect(ac, gDvxPortWindow, runX0 * scale, runY0 * scale, (runX1 - runX0) * scale, (y - runY0) * scale);
|
||||
if (runY0 >= 0 && show) {
|
||||
targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0);
|
||||
}
|
||||
runY0 = -1;
|
||||
continue;
|
||||
|
|
@ -321,11 +470,11 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidat
|
|||
x0 = gStageMinWord[y] * 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) {
|
||||
continue;
|
||||
}
|
||||
expandRow(src, y, x0, x1);
|
||||
expandRow(&t, src, y, x0, x1);
|
||||
gPresentCopiedBytes += (uint32_t)(x1 - x0);
|
||||
if (runY0 < 0) {
|
||||
runY0 = y;
|
||||
|
|
@ -336,13 +485,131 @@ static void presentRows(const jlSurfaceT *src, uint16_t palettes, bool invalidat
|
|||
runX1 = x1 > runX1 ? x1 : runX1;
|
||||
}
|
||||
}
|
||||
if (runY0 >= 0 && invalidate) {
|
||||
dvxInvalidateRect(ac, gDvxPortWindow, runX0 * scale, runY0 * scale, (runX1 - runX0) * scale, (y - runY0) * scale);
|
||||
if (runY0 >= 0 && show) {
|
||||
targetShow(&t, runX0, runY0, runX1 - runX0, y - runY0);
|
||||
}
|
||||
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
|
||||
// fits the desktop (up to DVX_SCALE_MAX), not resizable, under a Game menu.
|
||||
static bool windowCreate(AppContextT *ac) {
|
||||
|
|
@ -356,14 +623,14 @@ static bool windowCreate(AppContextT *ac) {
|
|||
MenuBarT *bar;
|
||||
MenuT *game;
|
||||
|
||||
frameW = 2 * CHROME_TOTAL_SIDE;
|
||||
frameH = CHROME_TOTAL_TOP + CHROME_MENU_HEIGHT + CHROME_TOTAL_BOTTOM;
|
||||
scaleW = (ac->display.width - frameW) / SURFACE_WIDTH;
|
||||
scaleH = (ac->display.height - frameH) / SURFACE_HEIGHT;
|
||||
gDvxPortScale = scaleW < scaleH ? scaleW : scaleH;
|
||||
gDvxPortScale = gDvxPortScale < 1 ? 1 : (gDvxPortScale > DVX_SCALE_MAX ? DVX_SCALE_MAX : gDvxPortScale);
|
||||
wantW = SURFACE_WIDTH * gDvxPortScale;
|
||||
wantH = SURFACE_HEIGHT * gDvxPortScale;
|
||||
frameW = 2 * CHROME_TOTAL_SIDE;
|
||||
frameH = CHROME_TOTAL_TOP + CHROME_MENU_HEIGHT + CHROME_TOTAL_BOTTOM;
|
||||
scaleW = (ac->display.width - frameW) / SURFACE_WIDTH;
|
||||
scaleH = (ac->display.height - frameH) / SURFACE_HEIGHT;
|
||||
gScale = scaleW < scaleH ? scaleW : scaleH;
|
||||
gScale = gScale < 1 ? 1 : (gScale > DVX_SCALE_MAX ? DVX_SCALE_MAX : gScale);
|
||||
wantW = SURFACE_WIDTH * gScale;
|
||||
wantH = SURFACE_HEIGHT * gScale;
|
||||
|
||||
win = dvxCreateWindowCentered(ac, appDescriptor.name, wantW + frameW, wantH + frameH, false);
|
||||
if (win == NULL) {
|
||||
|
|
@ -379,6 +646,7 @@ static bool windowCreate(AppContextT *ac) {
|
|||
bar = wmAddMenuBar(win);
|
||||
game = bar != NULL ? wmAddMenu(bar, "&Game") : NULL;
|
||||
if (game != NULL) {
|
||||
wmAddMenuCheckItem(game, "&Full Screen\tAlt+Enter", CMD_GAME_FULL_SCREEN, false);
|
||||
wmAddMenuItem(game, "&Help", CMD_GAME_HELP);
|
||||
wmAddMenuSeparator(game);
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -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) {
|
||||
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) -----
|
||||
|
||||
uint16_t jlpFrameCount(void) {
|
||||
|
|
@ -433,7 +722,13 @@ bool jlpInit(const jlConfigT *config) {
|
|||
gCloseRequested = false;
|
||||
gLutBits = 0;
|
||||
(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;
|
||||
} else if (gStagePaletteDirty) {
|
||||
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.
|
||||
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) {
|
||||
if (gDvxPortWindow != NULL) {
|
||||
dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow);
|
||||
gDvxPortWindow = NULL;
|
||||
}
|
||||
gFullWanted = false;
|
||||
gFullActive = false;
|
||||
}
|
||||
|
||||
|
||||
// Block until the clock's next frame boundary, the other programs running
|
||||
// meanwhile. Re-aiming the pacer at the frame now running makes every call
|
||||
// wait for the NEXT boundary, as a vertical blank wait does, instead of the
|
||||
// pacer's own catch-up rule.
|
||||
// In the window: block until the clock's next frame boundary, the other
|
||||
// programs running meanwhile. Re-aiming the pacer at the frame now running
|
||||
// makes every call wait for the NEXT boundary, as a vertical blank wait
|
||||
// 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) {
|
||||
gPacer.frame = clockFrame();
|
||||
(void)gameFrameWait(&gPacer);
|
||||
gYieldFrame = gPacer.frame;
|
||||
if (gFullActive) {
|
||||
tsYield();
|
||||
dvxExclusiveWaitRetrace();
|
||||
gYieldFrame = clockFrame();
|
||||
} else {
|
||||
gPacer.frame = clockFrame();
|
||||
(void)gameFrameWait(&gPacer);
|
||||
gYieldFrame = gPacer.frame;
|
||||
}
|
||||
dvxPortEndIfClosed();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@
|
|||
// 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
|
||||
// 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
|
||||
// and clamped to the stage, and published by the next poll, so the core's
|
||||
// button edges compare two stable snapshots.
|
||||
// Mouse: onMouse reports content-area pixels (screen pixels on the whole
|
||||
// screen); hal.c maps them to the stage pixel under them, and the next poll
|
||||
// publishes it, so the core's button edges compare two stable snapshots.
|
||||
//
|
||||
// Joysticks: DVX's joystick library, calibrated in DVX's Control Panel:
|
||||
// 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.
|
||||
#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 -----
|
||||
|
||||
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)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) {
|
||||
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)win;
|
||||
x /= gDvxPortScale;
|
||||
y /= gDvxPortScale;
|
||||
gNextMouseX = (int16_t)(x < 0 ? 0 : (x >= SURFACE_WIDTH ? SURFACE_WIDTH - 1 : x));
|
||||
gNextMouseY = (int16_t)(y < 0 ? 0 : (y >= SURFACE_HEIGHT ? SURFACE_HEIGHT - 1 : y));
|
||||
dvxPortStagePoint(&x, &y);
|
||||
gNextMouseX = (int16_t)x;
|
||||
gNextMouseY = (int16_t)y;
|
||||
gNextMouseButtons = buttons;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
//
|
||||
// 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
|
||||
// lines when it closes a port, so jlSerialClose drops DTR there (a modem
|
||||
// hangs up). A program handing a live call back to another has to keep the
|
||||
// port open.
|
||||
// rs232Open raises DTR and RTS; rs232Close leaves them as they are, so
|
||||
// jlSerialClose does not hang up a modem, and a port closed after
|
||||
// jlSerialSetDtr(false) stays down (scripts/verify-dvx-serial.sh checks
|
||||
// both at the far end).
|
||||
//
|
||||
// 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
|
||||
|
|
|
|||
80
src/x86/x86Expand.h
Normal file
80
src/x86/x86Expand.h
Normal 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
|
||||
Loading…
Add table
Reference in a new issue