diff --git a/.gitignore b/.gitignore index c08f495..c98f64e 100644 --- a/.gitignore +++ b/.gitignore @@ -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. diff --git a/README.md b/README.md index 3fc11f1..6a5f50f 100644 --- a/README.md +++ b/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//`, 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//`, +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 diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index 1df5aec..b26c35a 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -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 diff --git a/docs/ApiInput.adoc b/docs/ApiInput.adoc index bae26c6..7ec8994 100644 --- a/docs/ApiInput.adoc +++ b/docs/ApiInput.adoc @@ -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 diff --git a/docs/ApiSerial.adoc b/docs/ApiSerial.adoc index 6043906..21f2e22 100644 --- a/docs/ApiSerial.adoc +++ b/docs/ApiSerial.adoc @@ -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), diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc index 4b30ebd..20286cc 100644 --- a/docs/GettingStarted.adoc +++ b/docs/GettingStarted.adoc @@ -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/`: diff --git a/docs/Overview.adoc b/docs/Overview.adoc index 2370f8b..341e361 100644 --- a/docs/Overview.adoc +++ b/docs/Overview.adoc @@ -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 diff --git a/docs/Portability.adoc b/docs/Portability.adoc index bfa2d7c..2e422d5 100644 --- a/docs/Portability.adoc +++ b/docs/Portability.adoc @@ -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. diff --git a/examples/sertest/sertest.c b/examples/sertest/sertest.c index 67bd1d9..541b172 100644 --- a/examples/sertest/sertest.c +++ b/examples/sertest/sertest.c @@ -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 +#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 } diff --git a/include/joey/platform.h b/include/joey/platform.h index e748b7d..388b4cc 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -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 diff --git a/install.sh b/install.sh index 8ae1f7d..03c1e65 100755 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/installer/components/dvx.sh b/installer/components/dvx.sh index 5772d89..5890ce1 100644 --- a/installer/components/dvx.sh +++ b/installer/components/dvx.sh @@ -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 diff --git a/joey-run b/joey-run index 79b094b..9ed37b0 100755 --- a/joey-run +++ b/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 diff --git a/make/dvx.mk b/make/dvx.mk index cd9b03e..78df4db 100644 --- a/make/dvx.mk +++ b/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// -- 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)"' \ diff --git a/scripts/bench-dvx.sh b/scripts/bench-dvx.sh index 6901d1d..f28bfbb 100755 --- a/scripts/bench-dvx.sh +++ b/scripts/bench-dvx.sh @@ -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 diff --git a/scripts/lib/dvxGate.sh b/scripts/lib/dvxGate.sh index c07cbdb..b40d10a 100644 --- a/scripts/lib/dvxGate.sh +++ b/scripts/lib/dvxGate.sh @@ -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() { diff --git a/scripts/lib/keysRow.py b/scripts/lib/keysRow.py new file mode 100755 index 0000000..e82df42 --- /dev/null +++ b/scripts/lib/keysRow.py @@ -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)) diff --git a/scripts/lib/serialLoopPeer.py b/scripts/lib/serialLoopPeer.py index 9b7d2a2..ddcb383 100755 --- a/scripts/lib/serialLoopPeer.py +++ b/scripts/lib/serialLoopPeer.py @@ -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 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__": diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index b7e548a..e2675f1 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -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 diff --git a/scripts/verify-dvx-fullscreen.sh b/scripts/verify-dvx-fullscreen.sh new file mode 100755 index 0000000..5d4a5d7 --- /dev/null +++ b/scripts/verify-dvx-fullscreen.sh @@ -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)" diff --git a/scripts/verify-dvx-input.sh b/scripts/verify-dvx-input.sh index d366686..efe7e16 100755 --- a/scripts/verify-dvx-input.sh +++ b/scripts/verify-dvx-input.sh @@ -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)" diff --git a/scripts/verify-dvx-serial.sh b/scripts/verify-dvx-serial.sh index b9464fa..d53abc4 100755 --- a/scripts/verify-dvx-serial.sh +++ b/scripts/verify-dvx-serial.sh @@ -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 diff --git a/src/dos/hal.c b/src/dos/hal.c index b1426cc..2ec706c 100644 --- a/src/dos/hal.c +++ b/src/dos/hal.c @@ -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; } } diff --git a/src/dvx/dvxPort.h b/src/dvx/dvxPort.h index c32a76d..63a27fb 100644 --- a/src/dvx/dvxPort.h +++ b/src/dvx/dvxPort.h @@ -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); diff --git a/src/dvx/dvxSdk.h b/src/dvx/dvxSdk.h index 660113c..31e7a65 100644 --- a/src/dvx/dvxSdk.h +++ b/src/dvx/dvxSdk.h @@ -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" diff --git a/src/dvx/hal.c b/src/dvx/hal.c index 83d6a84..737fbca 100644 --- a/src/dvx/hal.c +++ b/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(); } diff --git a/src/dvx/input.c b/src/dvx/input.c index ef8ae76..b0155e2 100644 --- a/src/dvx/input.c +++ b/src/dvx/input.c @@ -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; } diff --git a/src/dvx/serial.c b/src/dvx/serial.c index cb63385..f51eb8c 100644 --- a/src/dvx/serial.c +++ b/src/dvx/serial.c @@ -5,10 +5,10 @@ // // DEFAULT, MODEM and PRINTER are COM1; SLOT is COM (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 diff --git a/src/x86/x86Expand.h b/src/x86/x86Expand.h new file mode 100644 index 0000000..c0e9b23 --- /dev/null +++ b/src/x86/x86Expand.h @@ -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 +#include + +#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