diff --git a/Makefile b/Makefile index 3dc0b30..a8212c0 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ # make atarist builds Atari ST only # make dos builds MS-DOS only # make x68000 builds Sharp X68000 only +# make dvx builds DVX (JoeyLib programs as DVX desktop programs) # make docs renders the manual to build/docs (HTML + PDF) # make verify builds every port, then runs every gate (needs emulators) # make verify-audio runs every port's audio gate (needs emulators) @@ -27,6 +28,8 @@ HAVE_AMIGA := $(if $(wildcard $(AMIGA_CC)),1) HAVE_ATARIST := $(if $(wildcard $(ST_CC)),1) HAVE_DOS := $(if $(wildcard $(DOS_CC)),1) HAVE_X68000 := $(if $(wildcard $(X68K_ROOT)/bin/m68k-xelf-gcc),1) +# DVX needs the DOS compiler and the DVX SDK the installer copies. +HAVE_DVX := $(if $(and $(wildcard $(DOS_CC)),$(wildcard $(DVX_SDK)/sdk.mk)),1) ALL_TARGETS := ifeq ($(HAVE_IIGS),1) @@ -44,8 +47,11 @@ endif ifeq ($(HAVE_X68000),1) ALL_TARGETS += x68000 endif +ifeq ($(HAVE_DVX),1) + ALL_TARGETS += dvx +endif -.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos x68000 blank-check tools clean help status verify verify-audio verify-serial docs +.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos x68000 dvx blank-check tools clean help status verify verify-audio verify-serial docs all: $(ALL_TARGETS) tools ifeq ($(ALL_TARGETS),) @@ -77,6 +83,9 @@ dos: x68000: @$(MAKE) -f $(REPO_DIR)/make/x68000.mk +dvx: + @$(MAKE) -f $(REPO_DIR)/make/dvx.mk + # Compile + link-check the new-port template (src/blank/blank.c) with the host # cc. No toolchain needed; keeps the template from bit-rotting as the lib grows. blank-check: @@ -120,9 +129,10 @@ status: @echo " atarist: $(if $(HAVE_ATARIST),yes,NO)" @echo " dos: $(if $(HAVE_DOS),yes,NO)" @echo " x68000: $(if $(HAVE_X68000),yes,NO)" + @echo " dvx: $(if $(HAVE_DVX),yes,NO)" help: - @echo "Targets: iigs amiga atarist dos x68000 all clean status help" + @echo "Targets: iigs amiga atarist dos x68000 dvx all clean status help" @echo " iigs-disk iigs-verify (IIgs MAME visual check)" @echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)" @echo " verify (build every port, then run every gate; PORTS= subset)" diff --git a/README.md b/README.md index 61a2e36..3fc11f1 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # JoeyLib A unified C game-development library targeting five early 16-bit -platforms from a single codebase: +platforms, and one DOS desktop, from a single codebase: - Apple IIgs (reference platform) - Commodore Amiga (A500 / 68000 baseline) - 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) The Apple IIgs defines the capability ceiling. Stronger platforms coast. Hot paths are hand-written assembly per port; the public API is C. @@ -46,7 +48,11 @@ the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`, `sertest`, `save`, `tilebank`, `joy`, `sprite`, `audio`, `uber`, `adventure`, `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. +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. 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 @@ -63,6 +69,7 @@ the emulators and their firmware: ./joey-run build/dos/bin/DRAW.EXE ./joey-run --port amiga draw ./joey-run --headless --seconds 30 --snapshot draw.png --port atarist draw +./joey-run --port dvx staxi # DVX in DOSBox, booting into the program ``` Four files are never downloaded -- the Amiga Kickstart, the Amiga @@ -80,6 +87,7 @@ make amiga make atarist make dos make x68000 +make dvx ``` Every fragment also answers to a bare `clean`, and every fragment that builds @@ -99,7 +107,9 @@ logs a surface hash per op. `tools/diff-uber-hashes ` compares two `joeylog.txt` captures and exits non-zero on any mismatch. The frozen references live in `tests/goldens/uber/`. All five ports currently agree on all 51 hashes; the X68000 has no golden of its own and is diffed against -`tests/goldens/uber/iigs.txt`, which it matches exactly: +`tests/goldens/uber/iigs.txt`, which it matches exactly, and DVX, which shows +the DOS stage, is diffed against `tests/goldens/uber/dos.txt` +(`scripts/bench-dvx.sh`, then `tools/diff-uber-hashes`): ``` make -f make/x68000.mk x68000-verify-golden # ~70 min under the patched MAME @@ -119,10 +129,12 @@ src/core/ portable library code src/generic/ portable-C default for every overridable op src/codegen/ shared sprite-codegen staging / compile machinery src// per-machine HAL + per-CPU sprite emitter - (iigs, amiga, atarist, dos, x68000) + (iigs, amiga, atarist, dos, x68000, dvx) src/m68k/ 68k code shared across machines (planar and word- interleaved sprite emitters; surface68k.s, which is linked by Amiga and Atari ST only) +src/x86/ x86 code DOS and DVX share (the sprite emitter, the + whole-map tile walker, the set-1 scan code table) src/blank/ copy-to-start template for a new port tools/assetbake/ PNG -> native .tbk / .spr baker (Python) tools/joeymod/ Protracker .MOD converter (passthrough or .NTP) @@ -187,7 +199,7 @@ void jlRandomSeed (uint32_t seed); All surfaces are 320x200 16-color images with a 200-entry SCB table and 16 palettes of 16 `$0RGB` colors. In-memory storage is -target-native: chunky 4bpp packed on IIgs and DOS, native planar +target-native: chunky 4bpp packed on IIgs, DOS and DVX, native planar (separate bitplanes on Amiga and Sharp X68000, word-interleaved planes on Atari ST) on the 68k ports. The public API speaks in color indices (0..15) and hides the storage format. @@ -648,7 +660,7 @@ from compiler-predefined macros is a fallback. Game code can conditionally compile on these: ``` -JOEYLIB_PLATFORM_IIGS / _AMIGA / _ATARIST / _DOS // exactly one defined +JOEYLIB_PLATFORM_IIGS / _AMIGA / _ATARIST / _DOS / _X68000 / _DVX // exactly one defined JOEYLIB_CPU_65816 / _68000 / _X86 JOEYLIB_ENDIAN_LITTLE / _BIG JOEYLIB_NATIVE_CHUNKY / _NATIVE_PLANAR diff --git a/docs/ApiAudio.adoc b/docs/ApiAudio.adoc index b893c4a..ef098bd 100644 --- a/docs/ApiAudio.adoc +++ b/docs/ApiAudio.adoc @@ -176,8 +176,8 @@ deactivates. [WARNING] ==== This is called from the audio engine's refill context -- the main thread on DOS -and ST, but the *audio interrupt* on Amiga and IIgs. It must not block and must -not allocate. +and ST (on DVX, DVX's own sound pump, while the program is yielded), but the +*audio interrupt* on Amiga and IIgs. It must not block and must not allocate. ==== === jlAudioPlaySfxStream @@ -274,6 +274,7 @@ What each port can actually do: |X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move all audio at once. |DOS, ST |Ignored. Output is mono. +|DVX |All three, through DVX's stereo mixer (on a stereo card). |=== === jlAudioStopSfx @@ -302,7 +303,8 @@ Direct hardware tone generator: plays a square wave continuously until the next call. Designed for AGI-era music where the engine programs a divisor and lets the chip play, so CPU cost during playback is zero. Single voice; uses voice 0 on platforms with several oscillators. On DOS this programs PIT counter 2 and -the speaker gate. +the speaker gate; on DVX it is a square tone of DVX's sound library, on the FM +chip when there is one. [cols="1,1,4", options="header"] |=== @@ -336,7 +338,8 @@ Three-voice PSG-style polyphonic tone generator. Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2; the ST uses YM2149 channels A, B and C; the Amiga uses Paula channels 0..2 looping a two-byte square sample; the IIgs uses Ensoniq DOC oscillators playing a -resident square. +resident square; DVX plays square tones of its sound library, which puts them +on the FM chip when there is a free voice and mixes them otherwise. Voice state is sticky -- a voice keeps its last frequency until you change it -- and CPU cost during sustained playback is zero, because the hardware @@ -348,7 +351,7 @@ The IIgs is the exception to stickiness: each call is an approximately 80 ms retriggered one-shot, so a note held longer must be re-asserted about once a frame. It also requires `jlAudioInit`, because the DOC path rides the tracker engine. On DOS, ST and Amiga this is direct hardware programming and works -without `jlAudioInit`. +without `jlAudioInit`; on DVX it opens the program's sound on first use. ==== === jlAudioNoise @@ -375,7 +378,7 @@ Mapping: the ST uses the YM2149 noise generator mixed onto channel C; DOS uses the OPL2 rhythm-mode snare, leaving the three tone channels untouched; the Amiga uses Paula channel 3 with an LFSR noise sample; the IIgs uses a resident noise sample on slot 3 with the same retriggered one-shot model as its tone -voices. +voices; DVX uses a noise tone of its sound library. [WARNING] ==== @@ -392,7 +395,7 @@ bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop); ---- Plays a JYM1 event stream baked by `songbake` through the tone and noise layer. -One authored song plays on all five ports at register-write cost. Because there +One authored song plays on every port at register-write cost. Because there is no CPU mixing, this is fully viable on the ST and cheap on DOS, which `.MOD` playback is not. @@ -516,6 +519,12 @@ On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick counter advancing at the canonical 18.2 Hz through an accumulator so `time` stays correct. +On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in +program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's +sound library, which `jlAudioVoice` and the rest go through there, may not be +called from an interrupt. `fn` may therefore do anything, but what it does +lands on the frame, not between frames. + === jlAudioCriticalEnter / jlAudioCriticalExit [source,c] @@ -527,6 +536,7 @@ void jlAudioCriticalExit(void); Critical section against the tick interrupt. If you mutate state that the registered tick callback reads, wrap the mutation in these so the interrupt cannot observe a half-written value. On DOS this is `cli` and `sti`; on ports -with no audio interrupt both are no-ops. +with no audio interrupt, and on DVX, where the callback runs in program time, +both are no-ops. *Parameters:* none. *Returns:* nothing. diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index 7225223..1df5aec 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -18,7 +18,7 @@ typedef struct { |`codegenBytes` |`uint32_t` |Size of the runtime compiled-sprite cache. `0` selects a per-platform - default: 8 KB on IIgs, 32 KB on Amiga, ST and DOS. On the IIgs the cache is + default: 8 KB on IIgs, 32 KB on Amiga, ST, DOS and DVX. On the IIgs the cache is one Memory Manager block that may not span a 64 KB bank boundary, so values above 65536 are clamped to 65536, and even a clamped request can fail if no free bank exists. Size it against `jlSpriteCompiledSize()`. @@ -78,8 +78,8 @@ const char *jlPlatformName(void); *Parameters:* none. -*Returns:* a human-readable platform identifier, for example `"Apple IIgs"` or -`"MS-DOS"`. Library-owned. +*Returns:* a human-readable platform identifier, for example `"Apple IIgs"`, +`"MS-DOS"` or `"DVX"`. Library-owned. === jlVersionString @@ -193,8 +193,9 @@ uint16_t jlFrameHz(void); *Parameters:* none. *Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs, -ST NTSC and X68000, 70 on VGA mode 13h. The real cadence may drift slightly; -this is the figure to divide by when converting frames to seconds. +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. === jlMillisElapsed @@ -203,8 +204,8 @@ this is the figure to divide by when converting frames to seconds. uint32_t jlMillisElapsed(void); ---- -Monotonic millisecond counter since `jlInit`. DOS and ST read a real hardware -timer; IIgs and Amiga derive it from the frame counter, so the value steps at +Monotonic millisecond counter since `jlInit`. DOS, ST and DVX read a real +hardware timer; IIgs and Amiga derive it from the frame counter, so the value steps at frame granularity there. Two calls separated by a long render report the true elapsed time, not the number of vertical blanks that fired. diff --git a/docs/ApiDebug.adoc b/docs/ApiDebug.adoc index 56faa1d..28e5166 100644 --- a/docs/ApiDebug.adoc +++ b/docs/ApiDebug.adoc @@ -2,10 +2,11 @@ Declared in ``. -A crash-tracing logger that writes to a file beside the executable. Debugging a -program on an emulated 1988 machine is awkward, and this is usually the fastest -way to find out what it did before it stopped doing it. The headless run -scripts retrieve the log automatically. +A crash-tracing logger that writes to a file beside the executable (on DVX, +in the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). +Debugging a program on an emulated 1988 machine is awkward, and this is +usually the fastest way to find out what it did before it stopped doing it. +The headless run scripts retrieve the log automatically. [NOTE] ==== diff --git a/docs/ApiFile.adoc b/docs/ApiFile.adoc index 9928a71..7797b5c 100644 --- a/docs/ApiFile.adoc +++ b/docs/ApiFile.adoc @@ -8,6 +8,12 @@ packager: * `DATA/` -- read-only game content: levels, sprites, audio. * `SAVES/` -- writable saves and preferences. +DVX is the exception, because its programs share one current directory: +there `DATA/` is beside the program, in its folder under DVX's `APPS`, and +`SAVES/` is in the program's own settings folder, `CONFIG\APPS\JOEYLIB\NAME`, +which DVX keeps apart from the program files so a reinstall never overwrites +it. The calls below find both there by themselves. + Both are addressed by a *bare name relative to their directory*. The prefix is added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself. diff --git a/docs/ApiInput.adoc b/docs/ApiInput.adoc index 08c6ac7..bae26c6 100644 --- a/docs/ApiInput.adoc +++ b/docs/ApiInput.adoc @@ -74,8 +74,8 @@ int jlInputGetChar(void); Pops the next typed character. Shift, caps lock and the keyboard layout have already been applied by the backend, so punctuation and shifted symbols arrive -correctly. The layout is the machine's own on IIgs, Amiga, ST and X68000; the -DOS port translates with a fixed US layout. +correctly. The layout is the machine's own on IIgs, Amiga, ST and X68000, and +DVX's own on DVX; the DOS port translates with a fixed US layout. Arrow keys are not characters and stay on the predicate API. @@ -138,8 +138,10 @@ bool jlMousePresent(void); *Parameters:* none. *Returns:* `true` if the platform confirmed a mouse at init. On DOS that means -the INT 33h driver reset successfully; on Amiga, ST, IIgs and X68000 the OS -mouse is always wired. +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. === jlMouseX / jlMouseY @@ -251,7 +253,9 @@ The next poll after this call captures the stick's *current* raw position as the new centre, so the user must be holding the stick centred when you call it. On Amiga, ST and DOS the sticks are genuinely digital and already report -1, 0 -or +1, so recalibration is a no-op and `deadZone` is ignored. The call still +or +1, so recalibration is a no-op and `deadZone` is ignored. DVX's sticks are +analog but calibrated, dead zone included, in DVX's own Control Panel, so the +call is a no-op there too. The call still clears any auto-disconnect state, so it is safe and correct to make it unconditionally. ==== diff --git a/docs/ApiSerial.adoc b/docs/ApiSerial.adoc index 5801ab1..6043906 100644 --- a/docs/ApiSerial.adoc +++ b/docs/ApiSerial.adoc @@ -20,14 +20,17 @@ One byte-stream API spans every port while the HAL hides very different hardware: the IIgs drives a Zilog 8530 SCC directly or a 6551 card in a slot; DOS uses an interrupt-driven 8250/16550 UART; the Amiga uses `serial.device`; the ST goes through the BIOS Aux channel; the X68000 transmits through IOCS -and receives on its own SCC interrupt. +and receives on its own SCC interrupt; DVX goes through DVX's serial library, +which owns the COM ports for every program on the desktop (a port another +program has open cannot be opened). The model is *non-blocking and buffered*. Received bytes accumulate in a background ring -- a hardware interrupt on DOS, the X68000 and the IIgs, the -OS on Amiga and ST -- and reads never block. Call `jlSerialPoll` once per +OS on Amiga, ST and DVX -- and reads never block. Call `jlSerialPoll` once per frame: it is next to a no-op where the interrupt or OS already fills the ring, and the hardware drain where nothing does, so calling it every frame is always -correct. +correct. On DVX it also gives the other programs their turn +(<<_dvx_programs>>). On the IIgs a Super Serial Card only interrupts with its interrupt switch (SW2-6) on; with it off the card still works, drained by `jlSerialPoll`, but @@ -57,6 +60,7 @@ at 9600 baud, so: |IIgs Super Serial Card (6551) |1 byte |~1 ms |DOS, 16550A UART |16 bytes, interrupt at 14 |~2 ms from the interrupt |DOS, 8250 or 16450 UART |1 byte |~1 ms +|DVX |as DOS: DVX's library drives the same UARTs |as DOS |Amiga (Paula) |1 byte |~1 ms |Atari ST (68901 MFP) |1 byte |~1 ms |X68000 (Zilog 8530) |3 bytes |~3 ms @@ -140,8 +144,8 @@ typedef struct { The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM` and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects -the IIgs modem port, DOS COM1, the ST Modem port, Amiga unit 0 or the X68000's -RS-232C port. +the IIgs modem port, DOS or DVX COM1, the ST Modem port, Amiga unit 0 or the +X68000's RS-232C port. On the X68000 the serial add-on *requires* Human68k's RS-232C driver, RSDRV.SYS. Every Human68k system disk has it and the stock CONFIG.SYS loads @@ -154,7 +158,8 @@ fills the receive ring. 38400 is really 39062 bps, 1.7% fast, within the usual tolerance of a UART at the far end. `unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super -Serial Card slot 1..7, a DOS COM number 1..4, or an Amiga `serial.device` unit. +Serial Card slot 1..7, a DOS or DVX COM number 1..4, or an Amiga +`serial.device` unit. === jlSerialOpen @@ -195,7 +200,9 @@ 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. +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. 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/Assets.adoc b/docs/Assets.adoc index cce118c..04c3521 100644 --- a/docs/Assets.adoc +++ b/docs/Assets.adoc @@ -125,7 +125,7 @@ one-time startup cost for a much smaller distribution. === Music: the Portable Chip Tracker -This is the recommended music path. One authored song plays on all five ports +This is the recommended music path. One authored song plays on every port at register-write cost, with no CPU mixing -- which matters enormously on the ST, where `.MOD` playback is only marginally viable. diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc index c86207f..d81ff75 100644 --- a/docs/BuildIntegration.adoc +++ b/docs/BuildIntegration.adoc @@ -55,14 +55,29 @@ MYGAME_SRCS := $(EXAMPLES)/mygame/mygame.c \ ---- Repeat for each port you want to support: `make/iigs.mk`, `make/amiga.mk`, -`make/atarist.mk`, `make/dos.mk` and `make/x68000.mk`. +`make/atarist.mk`, `make/dos.mk`, `make/x68000.mk` and `make/dvx.mk`. + +`make/dvx.mk` is the one exception to the pattern. A DVX program is a DVX SDK +project built by the SDK's own `sdk.mk`, which `make/dvx.mk` writes for every +name in its `APPS` list from three variables, plus a fourth for a program +with data files: + +[source,makefile] +---- +mygame_SRCS := $(EXAMPLES)/mygame/mygame.c $(EXAMPLES)/mygame/world.c +mygame_TITLE := My Game +mygame_HELP := One line on what the program is, for its help file. +mygame_DATA := $(DATADIR)/mygame/DATA # optional: staged by a rule of yours + +APPS := hello pattern ... mygame +---- [NOTE] ==== -Yes, that is five makefile edits for one program. The ports genuinely differ: +Yes, that is six makefile edits for one program. The ports genuinely differ: the IIgs needs OMF segmentation and a disk image, the Amiga a startup-sequence -entry, DOS a DPMI stub, and the X68000 a Human68k floppy layout. There is no -single rule that covers all five. +entry, DOS a DPMI stub, the X68000 a Human68k floppy layout, and DVX a module +with a help file. There is no single rule that covers them all. ==== === Naming @@ -78,6 +93,7 @@ File naming conventions differ per platform and the makefiles reflect that: |MS-DOS |Uppercase 8.3, `.EXE` |`MYGAME.EXE` |Amiga |Mixed case, no extension |`MyGame` |Sharp X68000 |Uppercase 8.3, `.X` |`MYGAME.X` +|DVX |Lowercase, 8 characters, a folder of its own |`apps/joeylib/mygame/mygame.app` |=== Keep the base name to 8 characters and letters and digits only, so the same @@ -114,6 +130,13 @@ vanish. *Amiga.* `joey-run` writes an `s/startup-sequence` that invokes your binary. +*DVX.* Every program is a folder laid out as it installs in DVX: +`build/dvx/bin/apps/joeylib/mygame/` holds `mygame.app`, its help file +`mygame.hlp` (DVX gives every program one; `make/dvx.mk` writes a one-topic +help from `mygame_HELP`) and its `DATA/`. Copy that folder into a DVX +installation's `APPS\JOEYLIB\` and start it from DVX, or let `joey-run` do it +(<<_dvx_programs>>). + === Verifying It Actually Runs Build success is weak evidence on retro targets. Run it with `joey-run` (see @@ -126,6 +149,7 @@ $ ./joey-run --port amiga mygame $ ./joey-run --port atarist mygame $ ./joey-run --port dos mygame $ ./joey-run --port x68000 mygame +$ ./joey-run --port dvx mygame ---- Without a window, which is what you want while iterating, run it headless and @@ -148,8 +172,8 @@ against your own program too. |`make verify-audio` |Every port's audio gate: the IIgs DOC and X68000 chips are read directly, - the ST's YM2149 register writes are traced, and the Amiga and DOS output is - recorded and graded. Exits 3, not 0, if a gate had to be skipped. + the ST's YM2149 register writes are traced, and the Amiga, DOS and DVX + output is recorded and graded. Exits 3, not 0, if a gate had to be skipped. |`make iigs-verify` |The IIgs actually reached the program and drew something. @@ -176,7 +200,8 @@ different pixels from the others. === Logging from the Target Debugging a program running on an emulated 1988 machine is awkward. JoeyLib -provides a logger that writes to a file next to the executable: +provides a logger that writes to a file next to the executable (on DVX, in +the program's settings folder): [source,c] ---- diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc index b474e60..4b30ebd 100644 --- a/docs/GettingStarted.adoc +++ b/docs/GettingStarted.adoc @@ -2,9 +2,10 @@ === Setting Up the Toolchains -JoeyLib cross-compiles for five machines. It builds on Linux x86-64, and on -Windows inside WSL2. One script installs every cross toolchain, pinned to a -known-good version and checked against a published checksum, into +JoeyLib cross-compiles for five machines and for DVX, a DOS desktop whose +programs run in windows (see <<_dvx_programs>>). It builds on Linux x86-64, +and on Windows inside WSL2. One script installs every cross toolchain, pinned +to a known-good version and checked against a published checksum, into `toolchains/` in the repository. The only things it installs anywhere else are ordinary packages from your distribution (apt on Debian and Ubuntu, dnf on Fedora), and only when you ask it to. @@ -55,6 +56,7 @@ Toolchain availability: atarist: yes dos: yes x68000: yes + dvx: yes ---- ==== Installer reference @@ -103,8 +105,8 @@ A target is a component name from `list`, or a group: | Target | Means | `toolchain:PORT` -| Everything needed to build for one port: `iigs`, `amiga`, `atarist`, `dos` -or `x68000`. +| Everything needed to build for one port: `iigs`, `amiga`, `atarist`, `dos`, +`x68000` or `dvx`. | `emulator:PORT` | Everything needed to run that port's programs: the emulator and its @@ -120,8 +122,9 @@ firmware. | Every port's emulator. | `all` -| Everything except optional components (such as `staxiData`, Space Taxi's -level source), which are installed by name. +| Everything except optional components (`staxiData`, Space Taxi's level +source, and `dvx`, which copies DVX from a DVX tree of yours), which are +installed by name. `toolchains` and `emulators` leave them out too. |=== [cols="1,3"] @@ -147,6 +150,13 @@ level source), which are installed by name. | Use an existing checkout of llvm816, the IIgs compiler, instead of cloning 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. |=== Everything the installer creates stays inside `toolchains/`: @@ -167,7 +177,7 @@ trees. | `toolchains/state/` | Which version of each component is installed, and settings such as -`--llvm816-dir`. +`--llvm816-dir` and `--dvx-dir`. | `toolchains/config.mk`, `toolchains/env.sh` | The tool paths, for `make` and for shells. @@ -189,6 +199,7 @@ $ make amiga # Commodore Amiga only $ make atarist # Atari ST only $ make dos # MS-DOS only $ make x68000 # Sharp X68000 only +$ make dvx # DVX only $ make clean # remove all build outputs ---- @@ -330,6 +341,7 @@ $ ./joey-run build/dos/bin/DRAW.EXE $ ./joey-run build/amiga/bin/Draw $ ./joey-run --port atarist draw $ ./joey-run build/iigs/bin/STAXI.2mg +$ ./joey-run --port dvx staxi ---- It recognises the machine from the file itself. Interactive runs use the @@ -382,6 +394,17 @@ default is 9600. pass arguments. |=== +==== DVX + +`joey-run` runs a DVX program in DOSBox Staging on a copy of the DVX the +installer took from a DVX tree (`./install.sh install dvx`), set up so DVX +boots straight into the program: it is DVX's desktop program, in its own +window on a 1024x768 desktop (640x480 with `--headless`), with a Sound +Blaster 16. The DVX of interactive runs is kept in `build/dvx/run`, so what +the program saves, in DVX's `CONFIG` folder, is there the next time; closing +DOSBox ends the run. When the program ends, DVX stays up with an empty +desktop. + A headless run with a snapshot is the quickest way to check a change: [source,console] @@ -442,6 +465,10 @@ arrives. To connect out, `socat` joins a pseudo-terminal to the network. | DOS | COM1 | DOSBox does both by itself. + +| DVX +| COM1 +| As on DOS; DVX's serial library drives the port. |=== `socat` comes from your distribution: `./install.sh deps host --install` @@ -512,3 +539,6 @@ built for every platform: |`spacetaxi` |A full port of Space Taxi: the largest example. |`uber` |The validation and benchmark harness. |=== + +On DVX every example is a folder of its own under `build/dvx/bin/apps/joeylib/` +with its help file (<<_dvx_programs>>). diff --git a/docs/Overview.adoc b/docs/Overview.adoc index 8465282..2370f8b 100644 --- a/docs/Overview.adoc +++ b/docs/Overview.adoc @@ -3,8 +3,9 @@ === What JoeyLib Is JoeyLib is a single-codebase C game library for five 16-bit and 32-bit retro -machines. You write one program against one API, and the same source compiles -and runs on all of them: +machines, and for DVX, a DOS desktop whose programs run in windows. You write +one program against one API, and the same source compiles and runs on all of +them: [cols="2,2,2,4", options="header"] |=== @@ -34,6 +35,12 @@ and runs on all of them: |68000 at 10 MHz |stock |Draws into the text plane, 320x200 centred in a 512x512 screen. + +|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>>. |=== The Apple IIgs is deliberately the slowest target and the design reference. If @@ -60,14 +67,14 @@ the public API, so you cannot corrupt memory by drawing off the edge. === What the Library Hides -The five machines store pixels in genuinely incompatible ways. JoeyLib keeps +The machines store pixels in genuinely incompatible ways. JoeyLib keeps that out of your program: [cols="2,3", options="header"] |=== |Platform |In-memory pixel storage -|Apple IIgs, MS-DOS +|Apple IIgs, MS-DOS, DVX |Chunky 4bpp packed: two pixels per byte, high nibble is the left pixel. |Commodore Amiga @@ -111,7 +118,7 @@ chapters are the whole programming model. library can load. <<_adding_your_program_to_the_build>> explains how to get your own program -compiling for all five targets. +compiling for every target. The API Reference documents every public function: its exact signature, each parameter, what it returns, and any per-platform behaviour you need to know diff --git a/docs/Portability.adoc b/docs/Portability.adoc index 92e8452..bfa2d7c 100644 --- a/docs/Portability.adoc +++ b/docs/Portability.adoc @@ -1,9 +1,9 @@ == Writing Portable Code A JoeyLib program that follows the rules in this chapter compiles and behaves -the same on all five machines. Most of the rules exist because one particular -target cannot do something the others can, and the Apple IIgs is usually that -target. +the same on all five machines and on DVX. Most of the rules exist because one +particular target cannot do something the others can, and the Apple IIgs is +usually that target. === The Rules, in Short @@ -141,7 +141,9 @@ you: * `SAVES/` -- writable saves and preferences, via the `jlSave*` calls Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never -`"DATA/levels/title.dat"`. +`"DATA/levels/title.dat"`. On DVX the two are not in the current directory +at all (<<_dvx_programs>>), so a program that opens `"DATA/..."` itself finds +nothing there. Keep names portable across the retro filesystems this library targets: *at most 8 characters plus a short extension, letters and digits only*, so they satisfy @@ -197,9 +199,9 @@ error. The API is uniform; these are the places where what you get differs. -[cols="2,2,2,2,2,2", options="header"] +[cols="2,2,2,2,2,2,2", options="header"] |=== -|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 +|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |Frame rate |60 Hz @@ -207,6 +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) |Pixel storage |chunky 4bpp @@ -214,6 +217,7 @@ The API is uniform; these are the places where what you get differs. |planar, interleaved |chunky 4bpp |text-plane planar +|chunky 4bpp |Stereo pan |all three @@ -221,6 +225,7 @@ The API is uniform; these are the places where what you get differs. |ignored (mono) |ignored (mono) |ignored +|all three |`jlAudioTone` |DOC oscillator @@ -228,6 +233,7 @@ The API is uniform; these are the places where what you get differs. |YM2149 |PIT counter 2 |- +|a DVX tone (FM chip or mixer) |Joystick |analog, auto-disables @@ -235,6 +241,7 @@ The API is uniform; these are the places where what you get differs. |digital |digital |digital +|analog (DVX calibrates) |Keyboard layout |machine's own @@ -242,6 +249,7 @@ The API is uniform; these are the places where what you get differs. |machine's own |fixed US |machine's own +|DVX's |Serial ports |modem, printer, slot card @@ -249,6 +257,7 @@ The API is uniform; these are the places where what you get differs. |Modem port |COM1..COM4 |- +|COM1..COM4 (DVX's library) |=== The joystick row deserves a note: the IIgs port *auto-disables* joystick @@ -271,8 +280,61 @@ Before you call a program portable, confirm: * [ ] Save file names are 8.3-safe. * [ ] Only one audio layer is active per scene. * [ ] `jlShutdown()` runs on every exit path. -* [ ] The program has actually been run on all five targets, not just built. +* [ ] The program has actually been run on every target, not just built. -That last item is the one that matters. All five ports have emulator run -scripts under `scripts/`, and building successfully proves considerably less -than running. +That last item is the one that matters. Every port has emulator run scripts +under `scripts/`, and building successfully proves considerably less than +running. + +=== DVX Programs + +DVX is a DOS desktop: a VESA graphics GUI with windows, cooperative +multitasking and its own libraries for sound, music, joysticks and serial +ports. On DVX a JoeyLib program is one of its programs, a `.app` module +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 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. +* *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 + `jlShutdown`, and its log is closed. +* *Sharing the machine.* DVX multitasks cooperatively: the other programs, + and this one's own window events, run only while it yields. `jlWaitVBL` + yields, and so do `jlInputPoll`, `jlStagePresent` and `jlSerialPoll`, at + most once a frame. A loop that calls none of them freezes the desktop until + it ends. +* *Files.* DVX's programs share one current directory, so JoeyLib roots its + files at run time: `DATA/` is found beside the program (its folder under + `APPS`), while `SAVES/` and `joeylog.txt` go to the program's own settings + folder, `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program + files so a reinstall never overwrites them. +* *Sound* goes through DVX's sound and music libraries, mixed with every + other program's: `.MOD` music, SFX in stereo, and the tone voices on the FM + chip when there is one. A `jlAudioTickRegister` callback runs in program + time, at the next `jlAudioFrameTick`, because DVX's sound library may not + 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. +* 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 +`./joey-run --port dvx NAME` starts one in DVX under DOSBox Staging. diff --git a/docs/ProgramLayout.adoc b/docs/ProgramLayout.adoc index e84c790..d615fa0 100644 --- a/docs/ProgramLayout.adoc +++ b/docs/ProgramLayout.adoc @@ -25,7 +25,7 @@ native sprite blitter code into. Getting it wrong is the most common cause of sprites drawing slowly, so it is worth understanding. * `0` selects a per-platform default: 8 KB on the IIgs, 32 KB on - Amiga, ST and DOS. + Amiga, ST, DOS and DVX. * The 68000 and x86 emitters produce code roughly four to eight times less dense than the 65816 one, so the same sprite needs far more arena space on those ports. A 32x32 sprite can need 8 to 16 KB there. @@ -140,9 +140,10 @@ rows. That is the one case where you have to mark the region yourself. ==== Frame-Rate Independence -The five targets do not share a refresh rate: 50 Hz (Amiga PAL), 60 Hz (IIgs, -ST NTSC, X68000) and 70 Hz (VGA mode 13h). A loop that moves a sprite "two -pixels per frame" runs 40 percent faster on DOS than on a PAL Amiga. +The targets do not share a refresh rate: 50 Hz (Amiga PAL), 60 Hz (IIgs, +ST NTSC, X68000) and 70 Hz (VGA mode 13h, and DVX's virtual rate). A loop +that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than +on a PAL Amiga. Two ways to handle it. Ask the frame rate and scale your per-frame step: @@ -181,7 +182,7 @@ it, but the buffers *you* allocated (including tile data you handed to === The Complete Skeleton -This compiles and runs on all five targets as-is. +This compiles and runs on every target as-is. [source,c] ---- diff --git a/examples/audio/audio.c b/examples/audio/audio.c index 237af91..3398933 100644 --- a/examples/audio/audio.c +++ b/examples/audio/audio.c @@ -16,6 +16,7 @@ #include #include +#include #include "demoSong.h" @@ -23,13 +24,15 @@ // test.mod -> test.ntp via joeymod at build time and ships the .NTP; // every other platform ships the raw .MOD. test.sfx is the same raw // PCM blob everywhere. The tracker song needs no per-platform form at -// all -- the baked JYM1 bytes are identical on every port. +// all -- the baked JYM1 bytes are identical on every port. The names +// are relative to DATA/ (jlDataOpen), which is not always the current +// directory's DATA/ (DVX: beside the program). #if defined(JOEYLIB_PLATFORM_IIGS) -# define TEST_MOD_PATH "DATA/TEST.NTP" +# define TEST_MOD_NAME "TEST.NTP" #else -# define TEST_MOD_PATH "DATA/test.mod" +# define TEST_MOD_NAME "test.mod" #endif -#define TEST_SFX_PATH "DATA/test.sfx" +#define TEST_SFX_NAME "test.sfx" // Slot 3: the tracker's three voices ride SFX slots 0-2 on the IIgs, // so the demo's one-shot SFX stays out of their way. @@ -46,16 +49,16 @@ #define BAR_H 16 -// Read an entire file into a freshly allocated buffer. Caller must +// Read an entire DATA/ file into a freshly allocated buffer. Caller must // free(*outBytes) once the audio engine has consumed the data. // Returns false if the file is missing, empty, or larger than maxLen. -static bool loadFile(const char *path, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen) { +static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen) { FILE *fp; long fileSize; uint8_t *buf; size_t readBytes; - fp = fopen(path, "rb"); + fp = jlDataOpen(name, "rb"); if (fp == NULL) { return false; } @@ -149,8 +152,8 @@ int main(void) { if (audioOk) { // Keep the MOD bytes resident so M can (re)start it later; // jlAudioPlayMod copies them at play time. - (void)loadFile(TEST_MOD_PATH, 64UL * 1024UL, &modBytes, &modLen); - (void)loadFile(TEST_SFX_PATH, 64UL * 1024UL, &sfxBytes, &sfxLen); + (void)loadFile(TEST_MOD_NAME, 64UL * 1024UL, &modBytes, &modLen); + (void)loadFile(TEST_SFX_NAME, 64UL * 1024UL, &sfxBytes, &sfxLen); } jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c index 10232d6..c52c47e 100644 --- a/examples/spacetaxi/spacetaxi.c +++ b/examples/spacetaxi/spacetaxi.c @@ -978,7 +978,7 @@ int main(void) { // gets one; whatever does not fit draws interpreted). The 68k ports' // cels are far larger (9-13 KB each) and their JIT is fast, so they // keep a modest arena and compile at level entry. -#if defined(JOEYLIB_PLATFORM_IIGS) || defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_PLATFORM_IIGS) || defined(JOEYLIB_CPU_X86) config.codegenBytes = 256UL * 1024; #else config.codegenBytes = 160UL * 1024; diff --git a/include/joey/platform.h b/include/joey/platform.h index 749b207..e748b7d 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -14,6 +14,7 @@ !defined(JOEYLIB_PLATFORM_ATARIST) && \ !defined(JOEYLIB_PLATFORM_DOS) && \ !defined(JOEYLIB_PLATFORM_X68000) && \ + !defined(JOEYLIB_PLATFORM_DVX) && \ !defined(JOEYLIB_PLATFORM_BLANK) #if defined(__DJGPP__) || defined(__MSDOS__) @@ -35,6 +36,10 @@ // JOEYLIB_PLATFORM_BLANK is the copy-to-start template for a new port (see // src/blank/blank.c). It has no auto-detect macro -- bring it up by building // with -DJOEYLIB_PLATFORM_BLANK. +// +// JOEYLIB_PLATFORM_DVX (a program running in a window of the DVX desktop, +// src/dvx/) is built with the same DJGPP compiler as DOS, so it cannot be +// told apart by compiler macros: make/dvx.mk passes -DJOEYLIB_PLATFORM_DVX. // ----- Validate exactly one platform is defined ----- @@ -43,6 +48,7 @@ defined(JOEYLIB_PLATFORM_ATARIST) + \ defined(JOEYLIB_PLATFORM_DOS) + \ defined(JOEYLIB_PLATFORM_X68000) + \ + defined(JOEYLIB_PLATFORM_DVX) + \ defined(JOEYLIB_PLATFORM_BLANK)) != 1 #error "JoeyLib: exactly one JOEYLIB_PLATFORM_* must be defined" #endif @@ -90,6 +96,13 @@ #define JOEYLIB_ENDIAN_BIG #define JOEYLIB_NATIVE_PLANAR #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. + #define JOEYLIB_CPU_X86 + #define JOEYLIB_ENDIAN_LITTLE + #define JOEYLIB_NATIVE_CHUNKY + #define JOEYLIB_PLATFORM_NAME "DVX" #elif defined(JOEYLIB_PLATFORM_BLANK) // Chunky little-endian is the simplest baseline for a new port: the generic // backend gives you fully-working software rendering out of the box. Adjust @@ -326,7 +339,7 @@ #elif defined(JOEYLIB_PLATFORM_DOS) // chunky generics: surface clear, draw pixel/line/circle, fill circle/rect, // tiles, surface readers, allocation -- DOS overrides the timing services - // plus the whole-map tile walker (src/dos/dosTile.h, dispatched via the + // plus the whole-map tile walker (src/x86/x86Tile.h, dispatched via the // jlpTileMapPaste alias from tile.c's shared whole-map arm like the ST // engine; not a jlp op, so no JL_HAS flag). // (The planar dual-write hooks are not linked to generics: every chunky port @@ -456,6 +469,57 @@ #define JL_HAS_AUDIO_PLAY_SFX #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: + // 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_INPUT_INIT + #define JL_HAS_INPUT_SHUTDOWN + #define JL_HAS_INPUT_POLL // held-key table, onKey characters, onMouse, joystick.lib + #define JL_HAS_JOYSTICK_RESET + #define JL_HAS_WAIT_VBL // gameFrameWait at a virtual 70 Hz (the DOS rate) + #define JL_HAS_FRAME_COUNT + #define JL_HAS_FRAME_HZ + #define JL_HAS_MILLIS_ELAPSED // platformMicros + // DVX programs share one current directory: DATA/ is found beside the + // .app, SAVES/ and joeylog.txt go in its settings folder (fileRoot.h). + #define JL_HAS_FILE_ROOTS + // save files + disk space (platformMkdirRecursive, remove, platformDiskSpace) + #define JL_HAS_SAVE_DIR_ENSURE + #define JL_HAS_SAVE_DELETE + #define JL_HAS_DISK_FREE + // audio through DVX's audio.lib and music.lib (src/dvx/audio.c). The tick + // callback runs in program time (audio.lib is not interrupt-safe), so the + // critical sections stay the generic no-ops. + #define JL_HAS_AUDIO_INIT + #define JL_HAS_AUDIO_SHUTDOWN + #define JL_HAS_AUDIO_PLAY_MOD + #define JL_HAS_AUDIO_STOP_MOD + #define JL_HAS_AUDIO_IS_PLAYING_MOD + #define JL_HAS_AUDIO_PLAY_SFX + #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_SET_PAN + #define JL_HAS_AUDIO_STOP_SFX + #define JL_HAS_AUDIO_TONE + #define JL_HAS_AUDIO_VOICE + #define JL_HAS_AUDIO_NOISE + #define JL_HAS_AUDIO_TICK_REGISTER + #define JL_HAS_AUDIO_FRAME_TICK + // serial add-on (optional; a COM port through DVX's rs232 library, which + // owns the UARTs for every program -- src/dvx/serial.c) + #define JL_HAS_SERIAL_OPEN + #define JL_HAS_SERIAL_CLOSE + #define JL_HAS_SERIAL_POLL + #define JL_HAS_SERIAL_READ + #define JL_HAS_SERIAL_WRITE + #define JL_HAS_SERIAL_AVAILABLE + #define JL_HAS_SERIAL_FLUSH + #define JL_HAS_SERIAL_SET_DTR + #define JL_HAS_SERIAL_SET_RTS #elif defined(JOEYLIB_PLATFORM_BLANK) // Copy-to-start template. It overrides ONLY the platform SERVICES (TODO // stubs in src/blank/blank.c). Everything graphical -- draw / tile / sprite diff --git a/install.sh b/install.sh index 06a1f75..8ae1f7d 100755 --- a/install.sh +++ b/install.sh @@ -23,8 +23,9 @@ # # Targets are component names (see `list`) or groups: # toolchain:PORT emulator:PORT PORT toolchains emulators all -# where PORT is iigs, amiga, atarist, dos or x68000. `all` leaves out -# optional components (such as staxiData); name those explicitly. +# 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. # # Options: # -y, --yes install missing host packages without asking @@ -34,6 +35,8 @@ # --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) # # Examples: # ./install.sh install toolchains every cross compiler @@ -54,7 +57,7 @@ source "$JOEY_INSTALLER/lib/config.sh" # shellcheck source=installer/lib/bundle.sh source "$JOEY_INSTALLER/lib/bundle.sh" -JOEY_PORTS="iigs amiga atarist dos x68000" +JOEY_PORTS="iigs amiga atarist dos x68000 dvx" depsInstall=0 @@ -77,16 +80,16 @@ groupMembers() { fi ;; toolchains) - if [[ "$groups" == *" toolchain:"* ]]; then + if [[ "$groups" == *" toolchain:"* && "$groups" != *" optional "* ]]; then printf '%s\n' "$id" fi ;; emulators) - if [[ "$groups" == *" emulator:"* ]]; then + if [[ "$groups" == *" emulator:"* && "$groups" != *" optional "* ]]; then printf '%s\n' "$id" fi ;; - iigs|amiga|atarist|dos|x68000) + iigs|amiga|atarist|dos|x68000|dvx) if [[ "$groups" == *" toolchain:$group "* || "$groups" == *" emulator:$group "* ]]; then printf '%s\n' "$id" fi @@ -417,6 +420,13 @@ while [ $# -gt 0 ]; do fi shift ;; + --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)" + mkdir -p "$JOEY_STATE" + (cd "$2" && pwd) > "$JOEY_STATE/dvxDir" + shift + ;; -h|--help|help) usage exit 0 diff --git a/installer/components/dosbox.sh b/installer/components/dosbox.sh index 10f6681..0f8a73f 100644 --- a/installer/components/dosbox.sh +++ b/installer/components/dosbox.sh @@ -1,9 +1,10 @@ -# DOSBox Staging 0.83.0: the DOS emulator, as the project's own prebuilt -# Linux release (bundled libraries), because Debian/Ubuntu only package the -# old 0.74 and the gates are written for Staging. +# DOSBox Staging 0.83.0: the DOS emulator (for DOS and for DVX, which runs +# on it), as the project's own prebuilt Linux release (bundled libraries), +# because Debian/Ubuntu only package the old 0.74 and the gates are written +# for Staging. -compDescription="emulator: DOSBox Staging 0.83.0 (MS-DOS; upstream Linux release)" -compGroups="emulator:dos" +compDescription="emulator: DOSBox Staging 0.83.0 (MS-DOS, DVX; upstream Linux release)" +compGroups="emulator:dos emulator:dvx" compRequires="host" compHostDeps="xz" compUrl="https://github.com/dosbox-staging/dosbox-staging/releases/download/v0.83.0/dosbox-staging-linux-x86_64-v0.83.0.tar.xz" diff --git a/installer/components/dvx.sh b/installer/components/dvx.sh new file mode 100644 index 0000000..5772d89 --- /dev/null +++ b/installer/components/dvx.sh @@ -0,0 +1,101 @@ +# 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. +# +# `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. + +compDescription="DVX: desktop runtime + SDK, copied from a DVX tree (--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="" + +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" +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 +} + + +compPin="dir:$dvxDir:$(dvxListing | sha1sum | cut -d' ' -f1)" + + +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" + 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 +} + + +compInstalled() { + [ -f "$compDir/dvx.exe" ] && + [ -f "$compDir/config/DVX.INI" ] && + [ -f "$compDir/$dvxSdk/sdk.mk" ] && + [ -x "$compDir/$dvxSdk/bin/dvxhlpc" ] && + [ -x "$compDir/$dvxSdk/bin/dvxres" ] +} + + +# 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. +compVerify() { + local tmp + local status=1 + verifyLinked "$compDir/$dvxSdk/bin" || return 1 + tmp="$(mktemp -d)" + cat > "$tmp/smoke.c" <<'EOF' +#include "dvxApp.h" +#include "shellApp.h" + +int32_t appMain(DxeAppContextT *ctx); + +SHELL_APP_DESCRIPTOR(.name = "Smoke", .hasMainLoop = false, .multiInstance = false, .stackSize = SHELL_STACK_DEFAULT, .priority = TS_PRIORITY_NORMAL); + + +int32_t appMain(DxeAppContextT *ctx) { + return dvxCreateWindowCentered(ctx->shellCtx, "Smoke", 200, 100, false) != NULL ? 0 : -1; +} +EOF + printf '.topic smoke.overview\n.title Smoke\n.toc 0 Smoke\n.default\n\n.h1 Smoke\n\nA test.\n' > "$tmp/smoke.dhs" + printf 'SDK_DIR = %s\nKIND = app\nNAME = smoke\ninclude $(SDK_DIR)/sdk.mk\n' "$compDir/$dvxSdk" > "$tmp/Makefile" + if make -s -C "$tmp" DJGPP_PREFIX="$JOEY_OPT/djgpp" > "$tmp/make.log" 2>&1 && [ -s "$tmp/smoke.app" ] && [ -s "$tmp/smoke.hlp" ]; then + status=0 + else + cat "$tmp/make.log" >&2 + fi + rm -rf "$tmp" + return $status +} + + +compEnv() { + envVar DVX_ROOT "$compDir" + envVar DVX_SDK "$compDir/$dvxSdk" +} diff --git a/joey-run b/joey-run index 2317219..79b094b 100755 --- a/joey-run +++ b/joey-run @@ -8,15 +8,16 @@ # PROGRAM is any of: # a program file build/dos/bin/DRAW.EXE, build/atarist/bin/DRAW.PRG, # build/amiga/bin/Draw, build/x68000/bin/DRAW.X, -# build/iigs/bin/DRAW -# an app folder build/amiga/bin/Taxi (the program plus its DATA) +# build/iigs/bin/DRAW, build/dvx/bin/apps/joeylib/draw/draw.app +# an app folder build/amiga/bin/Taxi (the program plus its DATA), +# build/dvx/bin/apps/joeylib/staxi # an IIgs disk image build/iigs/bin/STAXI.2mg (make iigs-game-disk), or # a data disk such as joey.2mg (boots GS/OS with it) # an example name draw, staxi, uber, ... together with --port # The machine is recognised from the file itself. # # Options: -# --port PORT iigs, amiga, atarist, dos or x68000 +# --port PORT iigs, amiga, atarist, dos, x68000 or dvx # --data DIR copy DIR's files into the program's DATA folder for # this run (for example an AGI game's files) # --headless no window and no sound, for scripts and CI; stops @@ -44,7 +45,14 @@ # The serial port is the machine's usual one: the IIgs modem port (the # printer port and a Super Serial Card are not connected), the X68000's # RS-232C port, the ST's Modem port, the Amiga's serial port (serial.device -# unit 0) and DOS's COM1. The link is raw TCP, without telnet negotiation. +# 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. # 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 @@ -134,6 +142,7 @@ magicPort() { 601a*) printf 'atarist\n' ;; 4d5a*) printf 'dos\n' ;; 4855*) printf 'x68000\n' ;; + 44584531*) printf 'dvx\n' ;; # "DXE1": a DVX module (.app) *) # Apple IIgs OMF: segment header byte 14 (NUMLEN) is 4 and # byte 15 (VERSION) is 2. @@ -559,6 +568,46 @@ runDos() { } +runDvx() { + local dir + local app + joeyNeed DOSBOX dosbox + joeyNeed DVX_ROOT dvx + app="$(dirname "$program")" + if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then + dir="$work/dvx" + else + dir="$repo/build/dvx/run" + # DVX's own desktop size, where a program's window is the stage at 3x. + DVX_VIDEO_W=1024 + DVX_VIDEO_H=768 + fi + # shellcheck source=scripts/lib/dvxGate.sh + source "$repo/scripts/lib/dvxGate.sh" + if [ -n "$dataDir" ]; then + stageFolder "$work/app/$(basename "$app")" + app="$work/app/$(basename "$app")" + fi + dvxStage "$dir" "$app" + dvxDosboxConf "$work/dosbox.conf" "$dir" sb16 + # transparent:1 -- data bytes only, as for DOS. + case "$serialMode" in + connect) printf '\n[serial]\nserial1 = nullmodem server:%s port:%s transparent:1\n' "$serialHost" "$serialPort" >> "$work/dosbox.conf" ;; + listen) printf '\n[serial]\nserial1 = nullmodem port:%s transparent:1\n' "$serialPort" >> "$work/dosbox.conf" ;; + esac + if [ "$headless" = "1" ]; then + startDisplay + fi + echo "joey-run: booting DVX in DOSBox; $(basename "$program") is its desktop program. Close DOSBox to stop." + serialNote "COM1" + XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" --conf "$work/dosbox.conf" \ + --set "mouse_capture=seamless" > "$work/dosbox.log" 2>&1 & + emuPid=$! + dosSerialCheck + waitOrStop +} + + runX68000() { local name local stage="$work/files" @@ -596,7 +645,7 @@ runX68000() { while [ $# -gt 0 ]; do case "$1" in - --port) port="${2:?--port needs iigs, amiga, atarist, dos or x68000}"; shift ;; + --port) port="${2:?--port needs iigs, amiga, atarist, dos, x68000 or dvx}"; shift ;; --data) dataDir="${2:?--data needs a folder}"; shift ;; --headless) headless=1 ;; --seconds) seconds="${2:?--seconds needs a number}"; shift ;; @@ -686,6 +735,7 @@ case "$detected" in atarist) runAtarist ;; dos) runDos ;; x68000) runX68000 ;; + dvx) runDvx ;; esac if [ -n "$snapshot" ] && [ -f "$snapshot" ]; then echo "joey-run: snapshot saved to $snapshot" diff --git a/make/common.mk b/make/common.mk index efefb8a..1ccae8b 100644 --- a/make/common.mk +++ b/make/common.mk @@ -28,12 +28,14 @@ HAVE_STAXI_DATA := $(if $(wildcard $(STAXI_RAW_DUMP)),1) staxiData = $(if $(HAVE_STAXI_DATA),$(1),$(staxiNote)) staxiNote = $(if $(staxiNoted),,$(eval staxiNoted := 1)$(info note: Space Taxi data skipped: stuff/spacetaxi/raw.bin is not present)) INCLUDE_DIR := $(REPO_DIR)/include -# Machine-specific code lives in src// (iigs, amiga, atarist, dos); -# code shared across machines of one CPU lives in a processor folder (src/m68k). +# Machine-specific code lives in src// (iigs, amiga, atarist, dos, +# x68000, dvx); code shared across machines of one CPU lives in a processor +# folder (src/m68k, src/x86). SRC_DIR := $(REPO_DIR)/src SRC_CORE := $(REPO_DIR)/src/core SRC_CG := $(REPO_DIR)/src/codegen SRC_68K := $(REPO_DIR)/src/m68k +SRC_X86 := $(REPO_DIR)/src/x86 EXAMPLES := $(REPO_DIR)/examples # The tile-bank example (examples/tilebank) loads the host gate's fixture, so diff --git a/make/dos.mk b/make/dos.mk index fed33b7..d26536b 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -16,7 +16,7 @@ BINDIR := $(BUILD)/bin # it; DOS never did). Frees EBP for allocation in the hot loops under # DOSBox's per-instruction-charged core (perf-hunt-2 rank 2). # so flag experiments never edit a tracked file (perf-hunt-2 rank 17). -CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(LIBXMP_DIR)/include -MMD -MP +CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(SRC_X86) -I$(LIBXMP_DIR)/include -MMD -MP ASFLAGS := -f coff LDFLAGS := @@ -30,10 +30,12 @@ LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP LIBXMP_AR := $(LIBDIR)/libxmplite.a LIBXMP_CFLAGS := -DJOEY_LIBXMP_LITE -DLIBXMP_CORE_PLAYER -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w -# The x86 sprite emitter (spriteEmitX86.c) now lives in src/dos/ and is built -# by the port rule below; only the CPU-independent dispatch stays in src/codegen. +# The x86 code DOS shares with DVX (the sprite emitter, the whole-map tile +# walker, the set-1 scan code table) lives in src/x86 and builds into obj/x86; +# only the CPU-independent sprite dispatch stays in src/codegen. PORT_C_SRCS := $(wildcard $(SRC_DIR)/dos/*.c) PORT_S_SRCS := $(wildcard $(SRC_DIR)/dos/*.asm) +X86_C_SRCS := $(wildcard $(SRC_X86)/*.c) CODEGEN_DIR := $(REPO_DIR)/src/codegen LIB_OBJS := \ @@ -41,6 +43,7 @@ LIB_OBJS := \ $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ $(patsubst $(SRC_DIR)/dos/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_DIR)/dos/%.asm,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ + $(patsubst $(SRC_X86)/%.c,$(BUILD)/obj/x86/%.o,$(X86_C_SRCS)) \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o @@ -156,9 +159,13 @@ STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN) MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel -.PHONY: all dos clean-dos clean +.PHONY: all dos dos-staxi-data clean-dos clean all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) +# Space Taxi's DATA tree alone: make/dvx.mk stages this same bake, since DVX +# runs the DOS stage and the same x86 code. +dos-staxi-data: $(call staxiData,$(STAXI_ASSET_DSTS)) + $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) -c $< -o $@ @@ -175,6 +182,10 @@ $(BUILD)/obj/port/%.o: $(SRC_DIR)/dos/%.asm @mkdir -p $(dir $@) $(DOS_AS) $(ASFLAGS) $< -o $@ +$(BUILD)/obj/x86/%.o: $(SRC_X86)/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ + $(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ diff --git a/make/dvx.mk b/make/dvx.mk new file mode 100644 index 0000000..cd9b03e --- /dev/null +++ b/make/dvx.mk @@ -0,0 +1,249 @@ +# 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 +# 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, +# LIBS = libjoey.a, DATA = its data files) and copied into build/dvx/bin/ +# laid out as it goes into DVX: apps/joeylib//.app, .hlp +# and DATA/. The gates and joey-run copy that tree into a DVX installation. + +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +# The asset rules below define real targets before `all`, which would +# otherwise become make's default goal. +.DEFAULT_GOAL := all + +$(call requireTool,$(DOS_CC),toolchain:dos) +$(call requireTool,$(DVX_SDK)/sdk.mk,toolchain:dvx) + +PLATFORM := dvx +BUILD := $(REPO_DIR)/build/$(PLATFORM) +LIBDIR := $(BUILD)/lib +BINDIR := $(BUILD)/bin +APPSDIR := $(BUILD)/apps +DATADIR := $(BUILD)/data + +# Where a program installs in DVX: apps///. +VENDOR := joeylib + +PORT_DIR := $(SRC_DIR)/dvx + +# sdk.mk's code generation, so the library matches the programs it links +# into; -fomit-frame-pointer as on DOS. +DVX_ARCH := -march=i486 -mtune=i586 -fomit-frame-pointer + +# The SDK's header folders (sdk.mk's list: all but the BASIC and socket ones), +# for the port's own files only -- the core never sees a DVX header. +DVX_SDK_INCS := $(addprefix -I,$(filter-out %/basic %/net,$(patsubst %/,%,$(wildcard $(DVX_SDK)/include/*/)))) + +# A program's exit() and atexit() are the port's (src/dvx/program.c): the +# handlers run, and JoeyLib shuts down, when the program ends rather than +# when DVX does. Applied to the library and the programs, never to the port. +PROGRAM_RENAMES := -Dexit=dvxPortExit -Datexit=dvxPortAtExit + +CFLAGS := $(COMMON_CFLAGS) $(DVX_ARCH) -DJOEYLIB_PLATFORM_DVX -I$(SRC_X86) -I$(SRC_CG) -MMD -MP +LIB_CFLAGS := $(CFLAGS) $(PROGRAM_RENAMES) +PORT_CFLAGS := $(CFLAGS) -I$(PORT_DIR) $(DVX_SDK_INCS) + +# The flags every program adds to sdk.mk's (which bring the SDK headers and +# -Werror), with the include folders every port gives its examples: the +# program's main becomes jlGameMain, which the port calls. +APP_CFLAGS := -DJOEYLIB_PLATFORM_DVX -Dmain=jlGameMain $(PROGRAM_RENAMES) -fomit-frame-pointer -I$(INCLUDE_DIR) -I$(SRC_CORE) -I$(PORT_DIR) + +# dvxApp.c is compiled into each program (it names it), not the library. +PORT_ENTRY := $(PORT_DIR)/dvxApp.c +PORT_C_SRCS := $(filter-out $(PORT_ENTRY),$(wildcard $(PORT_DIR)/*.c)) +X86_C_SRCS := $(wildcard $(SRC_X86)/*.c) + +LIB_OBJS := \ + $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \ + $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ + $(patsubst $(PORT_DIR)/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ + $(patsubst $(SRC_X86)/%.c,$(BUILD)/obj/x86/%.o,$(X86_C_SRCS)) \ + $(BUILD)/obj/codegen/spriteCompile.o \ + $(BUILD)/obj/codegen/spriteStage.o + +LIB := $(LIBDIR)/libjoey.a + +# The examples: for each, its sources, its window title and a line of help +# (no apostrophes: the help lines are written through the shell in quotes). +# Worth knowing which ones the gates use: +# UBER the golden-hash vehicle: a hash per public op, which must equal +# the DOS port's (tests/goldens/uber/dos.txt): the same stage. +# TILEBANK the tile-bank gate's program: the DOS bake loaded both ways and +# pixel-checked, every other target's bake refused. +# KEYS the input gate's program: typed characters to jlInputGetChar. +hello_SRCS := $(EXAMPLES)/hello/hello.c +hello_TITLE := Hello +hello_HELP := The smallest JoeyLib program: it clears the screen and waits for a key. +pattern_SRCS := $(EXAMPLES)/pattern/pattern.c +pattern_TITLE := Pattern +pattern_HELP := Eight bands of gradient palettes, one per band of scan lines. +draw_SRCS := $(EXAMPLES)/draw/draw.c +draw_TITLE := Draw +draw_HELP := The drawing primitives: pixels, lines, circles, rectangles and flood fills. +keys_SRCS := $(EXAMPLES)/keys/keys.c +keys_TITLE := Keys +keys_HELP := Keyboard, typed characters and mouse: held keys light up and typed text appears. Escape ends it. +serial_SRCS := $(EXAMPLES)/serial/serial.c +serial_TITLE := Serial +serial_HELP := A serial terminal on COM1. +sertest_SRCS := $(EXAMPLES)/sertest/sertest.c +sertest_TITLE := SerTest +sertest_HELP := The serial port test program. +save_SRCS := $(EXAMPLES)/save/save.c +save_TITLE := Save +save_HELP := Saved files: counts its runs in a file in its settings folder. +tilebank_SRCS := $(EXAMPLES)/tilebank/tilebank.c +tilebank_TITLE := TileBank +tilebank_HELP := The tile bank check: loads a baked tile bank two ways and checks every pixel. +joy_SRCS := $(EXAMPLES)/joy/joy.c +joy_TITLE := Joy +joy_HELP := Joysticks: shows the position and buttons of each joystick. +sprite_SRCS := $(EXAMPLES)/sprite/sprite.c +sprite_TITLE := Sprite +sprite_HELP := Compiled sprites moving over a background. +audio_SRCS := $(EXAMPLES)/audio/audio.c +audio_TITLE := Audio +audio_HELP := Sound: music, sound effects and tones. +uber_SRCS := $(EXAMPLES)/uber/uber.c +uber_TITLE := Uber +uber_HELP := The JoeyLib conformance and speed test: every drawing call, timed and checked. Its results go to joeylog.txt in its settings folder. +adv_SRCS := $(EXAMPLES)/adventure/adventure.c +adv_TITLE := Adventure +adv_HELP := A small adventure game. +adv2_SRCS := $(EXAMPLES)/adventure2/adventure2.c +adv2_TITLE := Adventure 2 +adv2_HELP := A second small adventure game. +agi_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c +agi_TITLE := AGI +agi_HELP := A Sierra AGI interpreter. The files of the game it plays go in its DATA folder. +staxi_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c +staxi_TITLE := Space Taxi +staxi_HELP := Space Taxi: fly the taxi, pick up the fares, take them where they ask to go. + +APPS := hello pattern draw keys serial sertest save tilebank joy sprite audio uber adv adv2 agi staxi + +# Data the examples read from DATA/ beside the program: AUDIO its music and +# sound effect; TILEBANK the DOS bake of the tile-bank fixture (the DOS +# target byte: DVX shows the DOS stage), every other target's bake it must +# refuse, and the expected pixels (common.mk); Space Taxi the DOS bake of +# its levels, compiled cel banks (x86 code, the DOS target byte) and speech, +# made by make/dos.mk -- only when the C64 image it comes from is present. +audio_DATA := $(DATADIR)/audio/DATA +tilebank_DATA := $(DATADIR)/tilebank/DATA +staxi_DATA := $(if $(HAVE_STAXI_DATA),$(DATADIR)/staxi/DATA) +AUDIO_DATA_FILES := $(audio_DATA)/test.mod $(audio_DATA)/test.sfx +TILEBANK_OWN := $(tilebank_DATA)/tilebank/own.tbk +DOS_STAXI_DATA := $(REPO_DIR)/build/dos/bin/STAXI/DATA + +.PHONY: all dvx dvx-lib dvx-apps clean-dvx clean $(addprefix app-,$(APPS)) + +all dvx: dvx-lib dvx-apps + +dvx-lib: $(LIB) + +dvx-apps: $(addprefix app-,$(APPS)) + +$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ + +$(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ + +$(BUILD)/obj/x86/%.o: $(SRC_X86)/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ + +$(BUILD)/obj/codegen/%.o: $(SRC_CG)/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(PORT_DIR)/%.c + @mkdir -p $(dir $@) + $(DOS_CC) $(PORT_CFLAGS) -c $< -o $@ + +$(LIB): $(LIB_OBJS) + @mkdir -p $(dir $@) + rm -f $@ + $(DOS_AR) rcs $@ $^ + +# One program: (re)write its SDK project -- the Makefile, symlinks to its +# sources and the port's entry, the help source and the resource list -- +# then let sdk.mk build and package it, and copy the package into bin/. +# sdk.mk rebuilds only what changed; the project files are rewritten only +# when their contents change, so an unchanged program is not relinked. +# $(1) = the program's name. +define appRule +app-$(1): $(LIB) $$($(1)_SRCS) $(PORT_ENTRY) $$(if $$($(1)_DATA),$(1)-data) + @mkdir -p $(APPSDIR)/$(1) + @for src in $$($(1)_SRCS) $(PORT_ENTRY); do ln -sfn $$$$src $(APPSDIR)/$(1)/$$$$(basename $$$$src); done + @printf '%s\n' \ + '# Generated by make/dvx.mk: the DVX SDK project of the JoeyLib example $(1).' \ + 'SDK_DIR = $(DVX_SDK)' \ + 'KIND = app' \ + 'NAME = $(1)' \ + 'VENDOR = $(VENDOR)' \ + 'LIBS = $(LIB)' \ + 'DATA = $$($(1)_DATA)' \ + "CFLAGS = $(APP_CFLAGS) -I$$(dir $$(firstword $$($(1)_SRCS))) -DDVX_PORT_APP_NAME='\"$$($(1)_TITLE)\"'" \ + 'include $$$$(SDK_DIR)/sdk.mk' | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/Makefile + @printf '%s\n' \ + '.topic $(1).overview' \ + '.title $$($(1)_TITLE)' \ + '.toc 0 $$($(1)_TITLE)' \ + '.default' \ + '.index $$($(1)_TITLE)' \ + '' \ + '.h1 $$($(1)_TITLE)' \ + '' \ + '$$($(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.' \ + | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).dhs + @printf '%s\n' \ + 'name text "$$($(1)_TITLE)"' \ + 'author text "JoeyLib"' \ + 'description text "JoeyLib example: $$($(1)_TITLE)"' \ + | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(1)/$(1).res + $$(MAKE) -C $(APPSDIR)/$(1) DJGPP_PREFIX=$(DOS_PREFIX) package + @mkdir -p $(BINDIR) + rm -rf $(BINDIR)/apps/$(VENDOR)/$(1) + cp -R $(APPSDIR)/$(1)/pkg/apps $(BINDIR)/ +endef +$(foreach a,$(APPS),$(eval $(call appRule,$(a)))) + +.PHONY: audio-data tilebank-data staxi-data +audio-data: $(AUDIO_DATA_FILES) +tilebank-data: $(TILEBANK_OWN) + +staxi-data: + $(MAKE) -f $(REPO_DIR)/make/dos.mk dos-staxi-data + rm -rf $(staxi_DATA) + @mkdir -p $(dir $(staxi_DATA)) + cp -R $(DOS_STAXI_DATA) $(staxi_DATA) + +$(audio_DATA)/%: $(REPO_DIR)/assets/% + @mkdir -p $(dir $@) + cp $< $@ + +$(TILEBANK_OWN): $(TILEBANK_BAKE_DEPS) + python3 $(TILEBANK_STAGE) dos $(TILEBANK_PNG) $(dir $@) + +clean-dvx: + rm -rf $(BUILD) + +# `clean` as an alias for `clean-dvx`. Without it a bare +# `make -f make/dvx.mk clean` FAILS -- and with stderr discarded that +# looks like success while leaving stale objects the next build links. +clean: clean-dvx + +# Pull in per-object header-dependency files generated by gcc -MMD/-MP. +-include $(LIB_OBJS:.o=.d) diff --git a/make/support/dvx/writeIfChanged.sh b/make/support/dvx/writeIfChanged.sh new file mode 100755 index 0000000..faa0c1c --- /dev/null +++ b/make/support/dvx/writeIfChanged.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Write standard input to FILE only when it differs from what FILE holds, so +# make/dvx.mk can regenerate a program's SDK project files on every build +# without touching their times (sdk.mk would otherwise rebuild the help and +# the resources each time). +# +# Usage: writeIfChanged.sh FILE + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $(basename "$0") FILE" >&2 + exit 2 +fi + +file="$1" +tmp="$(mktemp "$file.XXXXXX")" +cat > "$tmp" +chmod 644 "$tmp" +if cmp -s "$tmp" "$file"; then + rm -f "$tmp" +else + mv "$tmp" "$file" +fi diff --git a/make/tools.mk b/make/tools.mk index 1834d3a..d5ef5ec 100644 --- a/make/tools.mk +++ b/make/tools.mk @@ -65,9 +65,9 @@ $(MKSTLEVEL_BIN): $(MKSTLEVEL_SRC) @mkdir -p $(dir $@) $(HOST_CC) $(HOST_CFLAGS) $< -o $@ -$(BUILD_DIR)/spritebake-dos: $(SPRITEBAKE_SRC) $(SRC_DIR)/dos/spriteEmitX86.c +$(BUILD_DIR)/spritebake-dos: $(SPRITEBAKE_SRC) $(SRC_X86)/spriteEmitX86.c @mkdir -p $(dir $@) - $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_DOS $(SPRITEBAKE_INC) -I$(SRC_DIR)/dos $^ -o $@ + $(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_DOS $(SPRITEBAKE_INC) -I$(SRC_X86) $^ -o $@ $(BUILD_DIR)/spritebake-amiga: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c @mkdir -p $(dir $@) diff --git a/scripts/bench-dvx.sh b/scripts/bench-dvx.sh new file mode 100755 index 0000000..6901d1d --- /dev/null +++ b/scripts/bench-dvx.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Headless UBER run for DVX (DOSBox Staging, DVX's own emulator setup). +# +# Stages a scratch DVX (scripts/lib/dvxGate.sh) whose desktop is UBER, boots +# it in DOSBox Staging under Xvfb, and waits for UBER's "press any key to +# exit" line in its joeylog.txt -- which a DVX program writes to its settings +# folder, CONFIG\APPS\JOEYLIB\UBER on the staged drive. The log is copied to +# build/dvx/bin/joeylog.txt (tools/diff-uber-hashes and tools/uber-perf-table +# read it from there) and DOSBox is stopped. +# +# The stage is the DOS stage, so the hashes must equal the DOS port's: +# scripts/bench-dvx.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dvx/bin/joeylog.txt +# 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 + +set -euo 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 + +timeoutSeconds=${TIMEOUT:-600} +# The emulator's hard cap, a little past the wait, whatever this script does. +wallSeconds=$((timeoutSeconds + 30)) +out=$DVX_BIN/$DVX_LOG_NAME +doneLine="press any key to exit" + +dvxNeedBuild uber + +work=$(mktemp -d -t joeylib-bench-dvx.XXXXXX) +guard="" +dosbox="" +xvfb="" +# 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 + +dvxStage "$work/c" "$(dvxAppDir uber)" +dvxDosboxConf "$work/dosbox.conf" "$work/c" +mkdir -p "$work/xdg" +rm -f "$out" + +display=":$((RANDOM % 400 + 1700))" +Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +echo "bench-dvx: running UBER in DVX headless (timeout ${timeoutSeconds}s)..." >&2 +XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$wallSeconds" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & +guard=$! +dosbox=$(joeyChild "$guard" || true) + +start=$SECONDS +log=$(dvxAwaitLog "$work/c" uber "$doneLine" "$timeoutSeconds" "$guard" || true) +waited=$((SECONDS - start)) +if [ -z "$log" ]; then + log=$(dvxProgramLog "$work/c" uber) +fi +if [ -z "$log" ]; then + echo "bench-dvx: FAIL -- UBER wrote no joeylog.txt (DVX.LOG follows)" >&2 + dvxDvxLog "$work/c" >&2 + exit 1 +fi +tr -d '\r' < "$log" > "$out" +joeyStop "$dosbox" "$guard" 2> /dev/null +dosbox="" +guard="" + +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 +fi +echo "bench-dvx: captured $ops op lines in ${waited}s" >&2 +echo " -> $out" >&2 +grep "UBER:" "$out" | sed 's/ | hash=.*//' || true diff --git a/scripts/lib/dvxGate.sh b/scripts/lib/dvxGate.sh new file mode 100644 index 0000000..c07cbdb --- /dev/null +++ b/scripts/lib/dvxGate.sh @@ -0,0 +1,168 @@ +# 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 +# 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. + +DVX_GATE="$(basename "$0" .sh)" +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 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 +# mode, where a program's window shows the stage at 1x. A caller may set +# others before dvxStage (joey-run shows 1024x768, the stage at 3x). +DVX_VIDEO_W=${DVX_VIDEO_W:-640} +DVX_VIDEO_H=${DVX_VIDEO_H:-480} +DVX_VIDEO_BPP=${DVX_VIDEO_BPP:-16} +# DOSBox Staging as DVX's harness runs it: an S3 card (VBE 2.0 with a linear +# frame buffer, which DVX needs) and the host's full speed. +DVX_DOSBOX_MACHINE=svga_s3 +DVX_DOSBOX_MEM_MB=32 + + +# dvxAppDir PROGRAM -- print the built folder of a program (its make/dvx.mk +# name: uber, keys, ...): the .app, its help and its DATA. +dvxAppDir() { + printf '%s\n' "$DVX_BIN/apps/$DVX_VENDOR/$1" +} + + +# dvxNeedBuild PROGRAM... -- exit 2 unless each program is built. +dvxNeedBuild() { + local prog + for prog in "$@"; do + if [ ! -f "$(dvxAppDir "$prog")/$prog.app" ]; then + echo "$DVX_GATE: $(dvxAppDir "$prog")/$prog.app is missing; run: make dvx" >&2 + exit 2 + fi + done +} + + +# dvxStage DIR APPDIR... -- stage DVX in DIR/DVX with the programs in the +# APPDIRs (each a folder holding NAME.app, from dvxAppDir or anywhere else), +# installed as apps/joeylib/NAME. The first one is the desktop. DIR may hold +# an earlier staging: the runtime and the programs are copied over it, and +# what DVX wrote there (its settings, the programs' CONFIG folders) stays. +dvxStage() { + local dir="$1" + local part + local app + local name + shift + mkdir -p "$dir/DVX/apps/$DVX_VENDOR" + for part in $DVX_RUNTIME; do + cp -a "$DVX_ROOT/$part" "$dir/DVX/" + done + for app in "$@"; do + name="$(basename "$app")" + rm -rf "${dir:?}/DVX/apps/$DVX_VENDOR/$name" + cp -a "$app" "$dir/DVX/apps/$DVX_VENDOR/" + done + name="$(basename "$1")" + dvxIniSet "$dir/DVX/config/DVX.INI" "apps/$DVX_VENDOR/$name/$name.app" +} + + +# 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. +dvxIniSet() { + python3 - "$1" "$2" "$DVX_VIDEO_W" "$DVX_VIDEO_H" "$DVX_VIDEO_BPP" <<'PY' +import sys +path, desktop, width, height, bpp = sys.argv[1:] +sets = {("shell", "desktop"): desktop, ("video", "width"): width, ("video", "height"): height, ("video", "bpp"): bpp} +lines = open(path, newline="").read().split("\r\n") +out = [] +section = "" +for line in lines: + stripped = line.strip() + if stripped.startswith("["): + section = stripped.strip("[]").lower() + elif "=" in stripped and not stripped.startswith(";"): + key = stripped.split("=")[0].strip() + if section == "shell" and key.lower() == "welcome": + continue + if section == "sounds": + line = "%s =" % key + elif (section, key.lower()) in sets: + line = "%s = %s" % (key, sets[(section, key.lower())]) + out.append(line) +open(path, "w", newline="").write("\r\n".join(out)) +PY +} + + +# dvxDosboxConf FILE DIR [SBTYPE] -- a DOSBox Staging configuration that boots +# DIR/DVX as C:\DVX. SBTYPE (e.g. sb16) adds a Sound Blaster; without it the +# machine is silent (DVX then runs without sound, as on a PC without a card). +dvxDosboxConf() { + local sbType="${3:-none}" + local nosound=true + [ "$sbType" = none ] || nosound=false + cat > "$1" </dev/null +} + + +# dvxAwaitLog DIR PROGRAM REGEX SECONDS PID -- wait up to SECONDS (or until PID +# exits) for PROGRAM's log to hold a line matching REGEX; print its path if +# it does. +dvxAwaitLog() { + local log + local waited + for ((waited = 0; waited < $4; waited++)); do + log="$(dvxProgramLog "$1" "$2")" + if [ -n "$log" ] && tr -d '\r' < "$log" | grep -qE "$3"; then + printf '%s\n' "$log" + return 0 + fi + joeyAlive "$5" || return 1 + sleep 1 + done + return 1 +} + + +# 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() { + local log + log="$(find "$1/DVX" -maxdepth 1 -iname dvx.log -print -quit)" + [ -n "$log" ] && tr -d '\r' < "$log" | tail -"${2:-15}" + return 0 +} diff --git a/scripts/lib/joeyEnv.sh b/scripts/lib/joeyEnv.sh index b814324..b7e8e33 100644 --- a/scripts/lib/joeyEnv.sh +++ b/scripts/lib/joeyEnv.sh @@ -65,8 +65,14 @@ joeyExampleProgram() { atarist) ext=".PRG" ;; dos) ext=".EXE" ;; x68000) ext=".X" ;; + dvx) + # Every program is a folder of its own, as it installs in DVX. + up="${up,,}" + printf '%s\n' "$bin/apps/joeylib/$up/$up.app" + return 0 + ;; *) - echo "$(basename "$0"): unknown port '$2' (iigs, amiga, atarist, dos or x68000)" >&2 + echo "$(basename "$0"): unknown port '$2' (iigs, amiga, atarist, dos, x68000 or dvx)" >&2 return 2 ;; esac @@ -110,6 +116,25 @@ joeyAlive() { } +# joeyChild PID -- print the child of PID: the emulator a `timeout -s KILL` +# wrapper started. Stop the child first; stopping only the wrapper can orphan +# an emulator that ignores SIGTERM (Hatari does). +joeyChild() { + local tries + local child + + for ((tries = 0; tries < 20; tries++)); do + child="$(pgrep -P "$1" | head -1)" + if [ -n "$child" ]; then + printf '%s\n' "$child" + return 0 + fi + sleep 0.1 + done + return 1 +} + + # joeyStop PID... -- stop emulator processes and make sure they are gone. # Hatari, MAME and DOSBox Staging ignore or are slow to act on SIGTERM; a # plain `kill` left them running (and burning CPU) after their script had diff --git a/scripts/lib/tileBankGate.sh b/scripts/lib/tileBankGate.sh index 5c4a780..1d01be8 100644 --- a/scripts/lib/tileBankGate.sh +++ b/scripts/lib/tileBankGate.sh @@ -33,25 +33,6 @@ tileBankAwaitLine() { } -# tileBankChild PID -- print the child of PID: the emulator a `timeout -s KILL` -# wrapper started. Stop the child first; stopping only the wrapper can orphan -# an emulator that ignores SIGTERM (Hatari does). -tileBankChild() { - local tries - local child - - for ((tries = 0; tries < 20; tries++)); do - child="$(pgrep -P "$1" | head -1)" - if [ -n "$child" ]; then - printf '%s\n' "$child" - return 0 - fi - sleep 0.1 - done - return 1 -} - - # tileBankData BINDIR -- print the DATA/tilebank to stage: the one # make/.mk staged beside the program, or TILEBANK_DATA_DIR when set (a # negative control stages a deliberately broken bake that way). diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index 615d494..b7e548a 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -7,19 +7,22 @@ # host the shared layer on the host, no emulator: the new-port # template (blank), input, millis, save, serial, sfxmix, tilebank # golden UBER on each port, its 51 per-op surface hashes against -# tests/goldens/uber (x68000 against the IIgs's, its own gate) +# tests/goldens/uber (x68000 against the IIgs's, its own gate; +# dvx against the DOS port's, whose stage it shows) # visual iigs: every launchable example on joey.2mg boots and draws # 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 # host half is check-tilebank.sh under `host`) # input each port: a typed string reaches jlInputGetChar byte-exact -# audio five gates, two kinds: +# (dvx: and the present shows it, decoded from the desktop) +# audio six gates, two kinds: # iigs DOC wavetable + sound IRQ vector (register tap) # x68000 YM2151 noise + PPI port C + ADPCM rate (register tap) # atarist YM2149 write trace, with the pc (register tap) # amiga recorded sound, four loudness phases (Paula has no tap) # dos recorded sound, four loudness phases (SB has no tap) +# dvx recorded sound, four loudness phases (DVX's mixer, SB) # serial the host check of the shared layer, then each port's loopback # (banner out, probe echoed back): # host scripts/check-serial.sh (no emulator) @@ -28,6 +31,7 @@ # atarist Modem port -> Hatari pty # amiga serial.device -> FS-UAE TCP # dos COM1 -> DOSBox nullmodem +# dvx COM1 through DVX's rs232 library -> DOSBox nullmodem # mouse x68000: deltas, clamps and buttons through the IOCS mouse # (and with serial streaming both ways on the same SCC) # save iigs: a SAVES/ blob written and read back through GS/OS @@ -40,7 +44,7 @@ # loss probes and game disks for the IIgs), stopping on a build failure, then # runs every kind. A gate for a port that was not selected is left out. # -# SEQUENTIAL, deliberately. The two recording audio gates drive the emulator on +# SEQUENTIAL, deliberately. The recording audio gates drive the emulator on # wall-clock sleeps and grade fixed time windows of the capture, so a # second emulator competing for CPU can slide a phase boundary and fail a # gate that is actually fine. Running several emulators at once also makes @@ -62,7 +66,7 @@ # # Usage: scripts/verify-all.sh KIND|all [port ...] # with no ports, every port the kind has. Ports: host iigs -# x68000 atarist amiga dos +# x68000 atarist amiga dos dvx # Env: VERIFY_ALL_LOG also append the full transcript to this file # Requires: whatever each selected gate requires; missing ones SKIP. set -uo pipefail @@ -73,7 +77,7 @@ shift || true label="verify-all $kind" KINDS="host golden visual tilebank input audio serial mouse save loss disk" -ALL_PORTS="host iigs x68000 atarist amiga dos" +ALL_PORTS="host iigs x68000 atarist amiga dos dvx" # UBER on the Amiga needs longer than bench-amiga.sh's interactive default. AMIGA_UBER_SECONDS=400 @@ -91,28 +95,33 @@ golden x68000 uber scripts/verify-x68000-golden.sh golden atarist uber scripts/bench-atarist.sh && tools/diff-uber-hashes tests/goldens/uber/atarist.txt build/atarist/bin/joeylog.txt golden amiga uber TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && tools/diff-uber-hashes tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt golden dos uber scripts/bench-dos.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dos/bin/JOEYLOG.TXT +golden dvx uber scripts/bench-dvx.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dvx/bin/joeylog.txt visual iigs examples scripts/verify-iigs-all.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 tilebank amiga tilebank scripts/verify-amiga-tilebank.sh tilebank dos tilebank scripts/verify-dos-tilebank.sh +tilebank dvx tilebank scripts/verify-dvx-tilebank.sh input iigs input scripts/verify-iigs-input.sh input x68000 input scripts/verify-x68000-input.sh input atarist input scripts/verify-atarist-input.sh input amiga input scripts/verify-amiga-input.sh input dos input scripts/verify-dos-input.sh +input dvx input scripts/verify-dvx-input.sh audio iigs audio scripts/verify-iigs-audio.sh staxi audio x68000 audio scripts/verify-x68000-audio.sh audio atarist audio scripts/verify-atarist-audio.sh audio amiga audio scripts/verify-amiga-audio.sh audio dos audio scripts/verify-dos-audio.sh +audio dvx audio scripts/verify-dvx-audio.sh serial host serial scripts/check-serial.sh serial iigs serial scripts/verify-iigs-serial.sh serial x68000 serial scripts/verify-x68000-serial.sh serial atarist serial scripts/verify-atarist-serial.sh serial amiga serial scripts/verify-amiga-serial.sh serial dos serial scripts/verify-dos-serial.sh +serial dvx serial scripts/verify-dvx-serial.sh mouse x68000 mouse scripts/verify-x68000-mouse.sh mouse x68000 sermouse scripts/verify-x68000-serial-mouse.sh save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1 diff --git a/scripts/verify-amiga-tilebank.sh b/scripts/verify-amiga-tilebank.sh index 5da2492..8255844 100755 --- a/scripts/verify-amiga-tilebank.sh +++ b/scripts/verify-amiga-tilebank.sh @@ -67,7 +67,7 @@ DISPLAY="$display" timeout -s KILL "$wall" "$FSUAE" \ --fullscreen=0 \ --fade_out_duration=0 > "$work/fsuae.log" 2>&1 & guard=$! -fsuae=$(tileBankChild "$guard") +fsuae=$(joeyChild "$guard") log=$(tileBankAwaitLine "$work/hd" "$lineSeconds" "$guard") joeyStop "$fsuae" "$guard" 2> /dev/null diff --git a/scripts/verify-atarist-tilebank.sh b/scripts/verify-atarist-tilebank.sh index 2eeaabc..4ab2fea 100755 --- a/scripts/verify-atarist-tilebank.sh +++ b/scripts/verify-atarist-tilebank.sh @@ -56,7 +56,7 @@ SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout -s KILL "$wall" "$HATARI" \ --fast-forward on \ --auto 'C:\TILEBANK.PRG' > "$work/hatari.log" 2>&1 & guard=$! -hatari=$(tileBankChild "$guard") +hatari=$(joeyChild "$guard") log=$(tileBankAwaitLine "$work/c" "$lineSeconds" "$guard") joeyStop "$hatari" "$guard" 2> /dev/null diff --git a/scripts/verify-dos-tilebank.sh b/scripts/verify-dos-tilebank.sh index 7cde64b..806899a 100755 --- a/scripts/verify-dos-tilebank.sh +++ b/scripts/verify-dos-tilebank.sh @@ -61,7 +61,7 @@ XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ --conf "$conf" \ -c "MOUNT C \"$work/c\"" -c "C:" -c "TILEBANK.EXE" > "$work/dosbox.log" 2>&1 & guard=$! -dosbox=$(tileBankChild "$guard") +dosbox=$(joeyChild "$guard") log=$(tileBankAwaitLine "$work/c" "$lineSeconds" "$guard") joeyStop "$dosbox" "$guard" 2> /dev/null diff --git a/scripts/verify-dvx-audio.sh b/scripts/verify-dvx-audio.sh new file mode 100755 index 0000000..9b24ec2 --- /dev/null +++ b/scripts/verify-dvx-audio.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# verify-dvx-audio.sh - DVX audio gate, graded on recorded sound. +# +# The DVX port plays through DVX's own sound library (audio.lib, with its +# Sound Blaster driver and mixer) and music library (src/dvx/audio.c). +# Nothing in DOSBox can be tapped register by register, so, like the DOS and +# Amiga gates, this records what came out and grades it with +# tools/gradeAudioPhases.py, through a PulseAudio null sink made for this run +# (scripts/pulseRecordLib.sh: the host's own sound is never touched). +# +# AUDIO (examples/audio) is the desktop of a scratch DVX in DOSBox Staging +# with a Sound Blaster 16 (and its OPL3, where DVX plays square tones when +# it can). It is driven through the four phases the DOS gate uses: +# +# music the baked tracker song on entry (the jlAudioVoice tones), loud +# paused P pressed, near silence +# sfx SPACE during the pause, so any sound is the digital one-shot +# (DATA/test.sfx beside the program, through the DATA root) +# resumed P again, loud again +# +# Keys are HELD (keydown, pause, keyup): the demo tests jlKeyPressed, an +# edge polled once a frame. The thresholds sit at about half the levels +# measured (see the grader call below). +# +# Usage: scripts/verify-dvx-audio.sh +# Env: DVX_AUDIO_KEEP keep the work folder and the recording +# Requires: DOSBox Staging, Xvfb, xdotool, parec/pactl, python3, the dvx +# component; the audio program (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 +grader=$repo/tools/gradeAudioPhases.py +pulseLib=$repo/scripts/pulseRecordLib.sh +label=verify-dvx-audio +program=audio +wall=150 # hard cap on DOSBox, whatever happens to this script +readySeconds=60 # DVX boot until its shell reports ready +settleSeconds=4 # then AUDIO opening its window and starting the song + +# shellcheck source=scripts/pulseRecordLib.sh +. "$pulseLib" +for tool in Xvfb xdotool parec pactl python3; do + command -v "$tool" > /dev/null || { echo "$label: $tool not installed" >&2; exit 2; } +done +pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from" >&2; exit 2; } +dvxNeedBuild "$program" + +work=$(mktemp -d -t joeylib-dvx-audio.XXXXXX) +sinkModule="" +guard="" +dosbox="" +xvfb="" +parecPid="" +cleanup() { + [ -n "$parecPid" ] && kill "$parecPid" 2> /dev/null + joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null + # A leaked null sink on the user's audio server is the one side effect + # this gate could have. + pulseUnloadSink "$sinkModule" + if [ -n "${DVX_AUDIO_KEEP:-}" ]; then + echo "$label: kept $work" >&2 + else + rm -rf "$work" + fi + return 0 +} +trap cleanup EXIT + +sinkName="joeyDvxGate$$" +sinkModule=$(pulseCreateSink "$sinkName") || sinkModule="" +[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; } + +dvxStage "$work/c" "$(dvxAppDir "$program")" +dvxDosboxConf "$work/dosbox.conf" "$work/c" sb16 +mkdir -p "$work/xdg" + +display=":$((RANDOM % 400 + 2900))" +Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \ + timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & +guard=$! +dosbox=$(joeyChild "$guard") + +ready="" +for ((waited = 0; waited < readySeconds; waited++)); do + if dvxDvxLog "$work/c" 200 | grep -q "DVX Shell ready"; then + ready=1 + break + fi + joeyAlive "$guard" || break + sleep 1 +done +if [ -z "$ready" ]; then + echo "$label: FAIL - DVX did not come up within ${readySeconds}s (DVX.LOG follows)" >&2 + dvxDvxLog "$work/c" >&2 + exit 1 +fi +sleep "$settleSeconds" + +win=$(joeyFindWindow "$display" "$dosbox") +if [ -z "$win" ]; then + echo "$label: FAIL - no DOSBox window to send keys to" >&2 + exit 1 +fi + +parec --device="$sinkName.monitor" --format=s16le --rate=44100 --channels=2 \ + --file-format=wav "$work/out.wav" > "$work/parec.log" 2>&1 & +parecPid=$! + +DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null + +hold() { # $1 = key name + DISPLAY="$display" xdotool keydown --window "$win" "$1" + sleep 0.4 + DISPLAY="$display" xdotool keyup --window "$win" "$1" +} + +# The phase boundaries below are the timestamps handed to the grader, so +# the sleeps and the spec must stay in step (the DOS gate's schedule). +sleep 6 # 0-6s music +hold p # -> paused +sleep 8 # 6-14s paused +for _ in 1 2 3; do # 14-24s sfx into silence + hold space + sleep 1.5 +done +sleep 4 +hold p # -> resumed +sleep 9 # 24-33s resumed + +# parec writes the WAV header lazily; it must exit before the file parses. +kill "$parecPid" 2> /dev/null +wait "$parecPid" 2> /dev/null +parecPid="" +dvxDvxLog "$work/c" 200 | grep -E "^Audio:" | tail -3 + +[ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; } + +# Measured under DOSBox Staging 0.83 (SB16, DVX mixing at 22,050 Hz): music +# ~1260, paused 0, sfx ~670, resumed ~1245 -- the DOS port's levels, near +# enough, so the DOS gate's bounds (about half of each) apply unchanged. +python3 "$grader" "$label" "$work/out.wav" \ + "music:1:5:600:" \ + "paused:8:13::200" \ + "sfx:16:22:330:" \ + "resumed:27:32:600:" diff --git a/scripts/verify-dvx-input.sh b/scripts/verify-dvx-input.sh new file mode 100755 index 0000000..d366686 --- /dev/null +++ b/scripts/verify-dvx-input.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# verify-dvx-input.sh - Typed-character and present gate for DVX. Runs KEYS +# (examples/keys) as the desktop of a scratch DVX in DOSBox Staging under +# Xvfb and types the RetroNet definition-of-done string with xdotool (real X +# key events: ':' needs Shift, which DVX's keyboard layout applies before the +# window's onKey hands the character to jlInputGetChar). Two checks: +# 1. the present: a raw screenshot of the desktop must show KEYS's +# 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; +# 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. +# +# Usage: scripts/verify-dvx-input.sh +# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component; +# the keys program (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 + +program=keys +target="192.168.1.10:6510" +wall=180 # hard cap on DOSBox, whatever happens to this script +readySeconds=60 # DVX boot until its shell reports ready +settleSeconds=3 # then KEYS opening its window and taking the focus +logSeconds=30 # Escape until KEYS's typed line is in its log + +dvxNeedBuild "$program" + +work=$(mktemp -d -t joeylib-dvx-input.XXXXXX) +guard="" +dosbox="" +xvfb="" +# 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 + +dvxStage "$work/c" "$(dvxAppDir "$program")" +dvxDosboxConf "$work/dosbox.conf" "$work/c" +printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$work/shots" >> "$work/dosbox.conf" +mkdir -p "$work/xdg" "$work/shots" + +display=":$((RANDOM % 400 + 2500))" +Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & +guard=$! +dosbox=$(joeyChild "$guard") + +ready="" +for ((waited = 0; waited < readySeconds; waited++)); do + if dvxDvxLog "$work/c" 200 | grep -q "DVX Shell ready"; then + ready=1 + break + fi + joeyAlive "$guard" || break + sleep 1 +done +if [ -z "$ready" ]; then + echo "verify-dvx-input: FAIL - DVX did not come up within ${readySeconds}s (DVX.LOG follows)" >&2 + dvxDvxLog "$work/c" >&2 + exit 1 +fi +sleep "$settleSeconds" + +win=$(joeyFindWindow "$display" "$dosbox") +if [ -z "$win" ]; then + echo "verify-dvx-input: FAIL - DOSBox window not found" >&2 + exit 1 +fi +DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true +DISPLAY="$display" xdotool type --window "$win" --delay 150 -- "$target" +sleep 2 +# Ctrl+F5 = DOSBox screenshot hotkey. +DISPLAY="$display" xdotool key --window "$win" ctrl+F5 +sleep 2 +DISPLAY="$display" xdotool key --window "$win" Escape + +log=$(dvxAwaitLog "$work/c" "$program" '^typed=' "$logSeconds" "$guard") +cp "$log" "$work/joeylog.txt" 2> /dev/null +joeyStop "$dosbox" "$guard" 2> /dev/null +dosbox="" +guard="" + +png=$(ls "$work"/shots/*.png 2> /dev/null | head -1) +if [ -z "$png" ]; then + echo "verify-dvx-input: FAIL - no screenshot captured" >&2 + exit 1 +fi +if [ -z "$log" ]; then + echo "verify-dvx-input: FAIL - KEYS logged no typed line within ${logSeconds}s of Escape" >&2 + 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 diff --git a/scripts/verify-dvx-serial.sh b/scripts/verify-dvx-serial.sh new file mode 100755 index 0000000..b9464fa --- /dev/null +++ b/scripts/verify-dvx-serial.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# verify-dvx-serial.sh - both-directions RS-232 gate for the DVX port. +# +# Runs SERTEST (examples/sertest: banner, then echo) as the desktop of a +# scratch DVX in DOSBox Staging, with COM1 a nullmodem connecting to the +# host peer (scripts/lib/serialLoopPeer.py), which waits for the banner, +# sends a probe and expects it echoed. That exercises the chain: JoeyLib's +# serial layer -> the DVX port (src/dvx/serial.c) -> DVX's rs232 library and +# its interrupt -> DOSBox's UART -> host. +# +# 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 +# program (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 + +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 + +dvxNeedBuild "$program" + +work=$(mktemp -d -t joeylib-dvx-serial.XXXXXX) +peerPid="" +guard="" +dosbox="" +xvfb="" +# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed". +cleanup() { + joeyStop "$dosbox" "$guard" "$peerPid" "$xvfb" 2> /dev/null + rm -rf "$work" +} +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" +mkdir -p "$work/xdg" + +python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$peerWall" > "$work/peer.log" 2>&1 & +peerPid=$! +sleep 1 + +display=":$((RANDOM % 400 + 3300))" +Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$runSeconds" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & +guard=$! +dosbox=$(joeyChild "$guard") + +wait "$peerPid" +rc=$? +peerPid="" +joeyStop "$dosbox" "$guard" 2> /dev/null +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)" +else + echo "verify-dvx-serial: FAIL (DVX.LOG follows)" >&2 + dvxDvxLog "$work/c" >&2 +fi +exit "$rc" diff --git a/scripts/verify-dvx-tilebank.sh b/scripts/verify-dvx-tilebank.sh new file mode 100755 index 0000000..662ad58 --- /dev/null +++ b/scripts/verify-dvx-tilebank.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# verify-dvx-tilebank.sh - Tile-bank gate for DVX: proves the DVX port loads +# the DOS bake (chunky 4bpp, high nibble = left pixel -- DVX shows the DOS +# stage and takes the DOS target byte) through the run-time DATA root beside +# the program, and refuses a bank baked for any other target. +# +# Runs TILEBANK (examples/tilebank) as the desktop of a scratch DVX in DOSBox +# Staging, with the DATA/tilebank make/dvx.mk staged beside it from the host +# gate's fixture. The program loads its bake through jlTileBankLoadToSurface +# and through jlTileBankLoad + jlTilePaste, checks every pixel of both against +# the PNG, hashes both, and tries every other target's bake; its TILEBANK line +# in its joeylog.txt (in its settings folder) must say PASS with both hashes +# equal to the host gate's (scripts/check-tilebank.sh, run here for the value) +# and every other target refused. DOSBox is stopped as soon as the line +# appears. +# +# Usage: scripts/verify-dvx-tilebank.sh +# Env: TILEBANK_DATA_DIR stage this DATA/tilebank instead (negative controls) +# Requires: DOSBox Staging, Xvfb, a host cc, the dvx component; the tilebank +# program and its DATA/tilebank (make dvx). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/tileBankGate.sh" +source "$repo/scripts/lib/dvxGate.sh" +joeyNeed DOSBOX dosbox +joeyNeed DVX_ROOT dvx + +program=tilebank +programDir=$(dvxAppDir "$program") +dataDir=$(tileBankData "$programDir") +wall=150 # hard cap on DOSBox, whatever happens to this script +lineSeconds=120 # DVX boot plus the checks + +tileBankNeedBuild "$programDir/$program.app" "$dataDir" "make dvx" +tileBankReference + +work=$(mktemp -d -t joeylib-dvx-tilebank.XXXXXX) +guard="" +dosbox="" +xvfb="" +# 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 + +dvxStage "$work/c" "$(dvxAppDir "$program")" +staged=$work/c/DVX/apps/$DVX_VENDOR/$program/DATA/tilebank +rm -rf "$staged" +mkdir -p "$(dirname "$staged")" +cp -r "$dataDir" "$staged" +dvxDosboxConf "$work/dosbox.conf" "$work/c" +mkdir -p "$work/xdg" + +display=":$((RANDOM % 400 + 2100))" +Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 & +guard=$! +dosbox=$(joeyChild "$guard") + +log=$(dvxAwaitLog "$work/c" "$program" '^TILEBANK: ' "$lineSeconds" "$guard") +if [ -z "$log" ]; then + joeyStop "$dosbox" "$guard" 2> /dev/null + dosbox="" + guard="" + echo "verify-dvx-tilebank: FAIL - no TILEBANK line within ${lineSeconds}s (DVX.LOG follows)" >&2 + dvxDvxLog "$work/c" >&2 + exit 1 +fi +# Judged from a copy: the emulator is stopped first, as the other gates do. +cp "$log" "$work/joeylog.txt" +joeyStop "$dosbox" "$guard" 2> /dev/null +dosbox="" +guard="" +tileBankJudge "$work/joeylog.txt" diff --git a/src/codegen/spriteEmitChunky.h b/src/codegen/spriteEmitChunky.h index e27da28..76d850c 100644 --- a/src/codegen/spriteEmitChunky.h +++ b/src/codegen/spriteEmitChunky.h @@ -1,5 +1,5 @@ // Shared emit-time source-pixel helpers for the chunky 4bpp sprite -// emitters -- spriteEmitX86.c (DOS) and spriteEmitIigs.c. These two +// emitters -- spriteEmitX86.c (DOS, DVX) and spriteEmitIigs.c. These two // functions ARE the transparency/shift contract that the emitted // routines must share with the interpreted walkers bit-for-bit, so // they exist exactly once: a fix applied here reaches both CPUs. diff --git a/src/codegen/spriteEmitter.h b/src/codegen/spriteEmitter.h index 3189d5f..0fcd99c 100644 --- a/src/codegen/spriteEmitter.h +++ b/src/codegen/spriteEmitter.h @@ -1,10 +1,10 @@ // Internal interface for per-CPU sprite emitters. // -// Each per-CPU emitter file (src/dos/spriteEmitX86.c, +// Each per-CPU emitter file (src/x86/spriteEmitX86.c, // src/iigs/spriteEmitIigs.c, src/m68k/spriteEmitPlanar68k.c, // src/m68k/spriteEmitInterleaved68k.c) implements its own emit // functions. spriteCompile.c picks the right one at compile time -// (via #ifdef on JOEYLIB_PLATFORM_*) for the runtime build. +// (via #ifdef on JOEYLIB_PLATFORM_* / JOEYLIB_CPU_*) for the runtime build. // // Each emit function takes the sprite + shift variant and a capacity // `cap` (bytes available at `out`), and writes position-independent @@ -48,7 +48,7 @@ #define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86 10u #define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS 9u -#if defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_CPU_X86) #define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86 #elif defined(JOEYLIB_PLATFORM_IIGS) #define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS diff --git a/src/codegen/spriteStage.c b/src/codegen/spriteStage.c index dfade35..f9d2f06 100644 --- a/src/codegen/spriteStage.c +++ b/src/codegen/spriteStage.c @@ -17,11 +17,11 @@ // Compile-time selection of the per-CPU emitter. One per-CPU -// spriteEmit*.c file (src/dos, src/iigs, src/m68k) is built per +// spriteEmit*.c file (src/x86, src/iigs, src/m68k) is built per // platform, but the dispatch lives in this file so jlSpriteCompile + // spriteCompiledDraw aren't duplicated per port. static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { -#if defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_CPU_X86) return spriteEmitDrawX86(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) return spriteEmitDrawPlanar68k(out, cap, sp, shift); @@ -41,7 +41,7 @@ static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *s // != SPRITE_NOT_COMPILED and falls back to the interpreted memcpy // path otherwise. static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { -#if defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_CPU_X86) return spriteEmitSaveX86(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) return spriteEmitSavePlanar68k(out, cap, sp, shift); @@ -57,7 +57,7 @@ static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *s static uint32_t emitRestoreForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { -#if defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_CPU_X86) return spriteEmitRestoreX86(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) return spriteEmitRestorePlanar68k(out, cap, sp, shift); diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c index 7317fb6..150d6b9 100644 --- a/src/core/assetLoad.c +++ b/src/core/assetLoad.c @@ -26,6 +26,7 @@ #include "joey/file.h" #include "joey/sprite.h" #include "joey/tile.h" +#include "fileRoot.h" #include "port.h" #include "spriteInternal.h" #include "spritePrecompiled.h" @@ -93,8 +94,8 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1]; #define EXPECTED_TILE_TARGET 1u #elif defined(JOEYLIB_PLATFORM_ATARIST) #define EXPECTED_TILE_TARGET 2u -#elif defined(JOEYLIB_PLATFORM_DOS) -#define EXPECTED_TILE_TARGET 3u +#elif defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_DVX) +#define EXPECTED_TILE_TARGET 3u // DVX shows the DOS stage: same chunky banks #elif defined(JOEYLIB_PLATFORM_IIGS) #define EXPECTED_TILE_TARGET 4u #elif defined(JOEYLIB_PLATFORM_BLANK) @@ -115,8 +116,8 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1]; #define EXPECTED_SPC_TARGET 1u #elif defined(JOEYLIB_PLATFORM_ATARIST) #define EXPECTED_SPC_TARGET 2u -#elif defined(JOEYLIB_PLATFORM_DOS) || defined(JOEYLIB_PLATFORM_BLANK) -#define EXPECTED_SPC_TARGET 3u +#elif defined(JOEYLIB_CPU_X86) || defined(JOEYLIB_PLATFORM_BLANK) +#define EXPECTED_SPC_TARGET 3u // x86 code: DOS and DVX run the same banks #elif defined(JOEYLIB_PLATFORM_IIGS) #define EXPECTED_SPC_TARGET 4u #elif defined(JOEYLIB_PLATFORM_X68000) @@ -181,9 +182,17 @@ static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) { // loader and example routes file opens through this, so callers never // hard-code "DATA/" and the runtime tree layout stays consistent across // ports. Returns NULL if the joined path overflows DATA_PATH_MAX or the -// file cannot be opened. +// file cannot be opened. A port with run-time roots (fileRoot.h) puts +// DATA/ under its read root. FILE *jlDataOpen(const char *name, const char *mode) { char path[DATA_PATH_MAX]; +#if defined(JL_HAS_FILE_ROOTS) + + if (name == NULL || !fileRootJoin(path, sizeof(path), jlpReadRoot(), DATA_DIR_PREFIX, name)) { + return NULL; + } + return fopen(path, mode); +#else size_t prefixLen = sizeof(DATA_DIR_PREFIX) - 1u; // literal length, no NUL size_t nameLen; @@ -202,6 +211,7 @@ FILE *jlDataOpen(const char *name, const char *mode) { memcpy(path, DATA_DIR_PREFIX, prefixLen); memcpy(path + prefixLen, name, nameLen + 1u); // copy the NUL too return fopen(path, mode); +#endif } diff --git a/src/core/debug.c b/src/core/debug.c index 627bea8..e7be433 100644 --- a/src/core/debug.c +++ b/src/core/debug.c @@ -24,6 +24,7 @@ #include "joey/platform.h" #include "joey/debug.h" +#include "fileRoot.h" static const char *kLogPath = "joeylog.txt"; @@ -154,26 +155,68 @@ void jlLogReset(void) { #else /* not JOEYLIB_PLATFORM_IIGS */ static FILE *gLogFp = NULL; +#if defined(JL_HAS_FILE_ROOTS) +// The log FILE belongs to the process hosting the program and can outlive +// it (a program ended by force never reaches jlLogClose), so it keeps the +// host's own buffer rather than one inside the program. (DVX does not even +// offer its programs setvbuf.) jlLog/jlLogF still flush once per line. +#define LOG_PATH_MAX 256 +#else /* 16 KB full buffer so a single log line's many small vfprintf writes * coalesce into one host write; jlLog/jlLogF then fflush once per * line. Default stdio buffers are only ~512 bytes, which would split a * long line across multiple writes. */ #define JOEY_LOG_BUF_BYTES 16384 static char gLogBuf[JOEY_LOG_BUF_BYTES]; +#endif + +// ----- Prototypes ----- + +static void logClose(void); +static FILE *logFile(void); +#if defined(JL_HAS_FILE_ROOTS) +static FILE *logOpen(const char *mode); +#else +// joeylog.txt is relative to the current directory. +#define logOpen(_mode) fopen(kLogPath, (_mode)) +#endif + + +static void logClose(void) { + if (gLogFp != NULL) { + fclose(gLogFp); + gLogFp = NULL; + } +} /* Lazy-open. Returns NULL if the open failed (silently disable). */ static FILE *logFile(void) { if (gLogFp == NULL) { - gLogFp = fopen(kLogPath, "a"); + gLogFp = logOpen("a"); +#if !defined(JL_HAS_FILE_ROOTS) if (gLogFp != NULL) { (void)setvbuf(gLogFp, gLogBuf, _IOFBF, sizeof(gLogBuf)); } +#endif } return gLogFp; } +#if defined(JL_HAS_FILE_ROOTS) +// joeylog.txt under the port's write root (fileRoot.h). +static FILE *logOpen(const char *mode) { + char path[LOG_PATH_MAX]; + + if (!fileRootJoin(path, sizeof(path), jlpWriteRoot(), kLogPath, "")) { + return NULL; + } + return fopen(path, mode); +} +#endif + + void jlLog(const char *msg) { FILE *fp; if (msg == NULL) { @@ -193,6 +236,13 @@ void jlLog(const char *msg) { } +#if defined(JL_HAS_FILE_ROOTS) +void jlLogClose(void) { + logClose(); +} +#endif + + void jlLogF(const char *fmt, ...) { FILE *fp; va_list args; @@ -241,14 +291,11 @@ void jlLogFlush(void) { void jlLogReset(void) { - if (gLogFp != NULL) { - fclose(gLogFp); - gLogFp = NULL; - } + logClose(); /* Truncate by opening for write then closing; subsequent * jlLog* will reopen for append. */ { - FILE *fp = fopen(kLogPath, "w"); + FILE *fp = logOpen("w"); if (fp != NULL) { fclose(fp); } diff --git a/src/core/fileRoot.h b/src/core/fileRoot.h new file mode 100644 index 0000000..ee415b5 --- /dev/null +++ b/src/core/fileRoot.h @@ -0,0 +1,46 @@ +// Run-time file roots (JL_HAS_FILE_ROOTS). +// +// Every machine port runs its program with the program's own folder as the +// current directory, so the core opens DATA/, SAVES/ and joeylog.txt by +// those relative names (literals in assetLoad.c, save.c and debug.c). A port +// whose programs share one current directory with other programs and end +// without ending the process (DVX: each program is a module loaded into the +// desktop) defines JL_HAS_FILE_ROOTS in platform.h and supplies the folders +// they live in. Without the flag this header declares nothing and the core +// keeps its literals, so no other port's code changes. +#ifndef JOEYLIB_FILE_ROOT_H +#define JOEYLIB_FILE_ROOT_H + +#include "joey/platform.h" + +#if defined(JL_HAS_FILE_ROOTS) + +#include +#include +#include + +// The roots are folder prefixes: "" or a path ending in a separator, valid +// from before the program's main runs until it ends. +// jlpReadRoot holds DATA/, the program's own read-only files +// jlpWriteRoot holds SAVES/ and joeylog.txt; the port creates the folder +// the first time it is asked for it +const char *jlpReadRoot(void); +const char *jlpWriteRoot(void); + +// Close joeylog.txt (debug.c). The port calls it when its program ends: +// the file handle belongs to the host process, which lives on. A later +// jlLog opens the file again, appending. +void jlLogClose(void); + + +// out = root + rel + name. False when that does not fit in cap bytes. +static inline bool fileRootJoin(char *out, size_t cap, const char *root, const char *rel, const char *name) { + int written; + + written = snprintf(out, cap, "%s%s%s", root, rel, name); + return written >= 0 && (size_t)written < cap; +} + +#endif + +#endif diff --git a/src/core/port.h b/src/core/port.h index 8046fc9..d4197c1 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -418,10 +418,10 @@ void iigsIrqLatencyTighten(void); #define jlpTilePasteGlyph planarTilePasteGlyph #define jlpTileSnap planarTileSnap #define jlpTileMapPaste planarTileMapPaste -#elif defined(JOEYLIB_PLATFORM_DOS) - // DOS overrides only the whole-map walker (dosTile.h, W3); the - // single-tile ops stay on the chunky generics. - #define jlpTileMapPaste dosTileMapPaste +#elif defined(JOEYLIB_CPU_X86) + // DOS and DVX override only the whole-map walker (src/x86/x86Tile.h, + // W3); the single-tile ops stay on the chunky generics. + #define jlpTileMapPaste x86TileMapPaste #endif // ===================================================================== @@ -1136,14 +1136,14 @@ void jlpGenericBigFree(void *p); // Per-port tile-op inline bodies (see the alias block above the // dispatch tail). The 68k headers are included LAST so their // portData == NULL fallbacks can call the jlpGenericTile* prototypes -// declared above; the DOS header (whole-map engine only, W3) needs +// declared above; the x86 header (whole-map engine only, W3) needs // nothing from the tail but sits here for the same one-include-point. #if defined(JOEYLIB_PLATFORM_ATARIST) #include "stTile.h" #elif defined(JOEYLIB_PLATFORM_AMIGA) #include "amigaTile.h" -#elif defined(JOEYLIB_PLATFORM_DOS) - #include "dosTile.h" +#elif defined(JOEYLIB_CPU_X86) + #include "x86Tile.h" #elif defined(JOEYLIB_PLATFORM_X68000) #include "x68kTile.h" #endif diff --git a/src/core/save.c b/src/core/save.c index 82c8468..79cd635 100644 --- a/src/core/save.c +++ b/src/core/save.c @@ -11,6 +11,7 @@ #include #include "joey/file.h" +#include "fileRoot.h" #include "port.h" @@ -30,17 +31,40 @@ // ----- Helpers ----- static bool buildSavePath(char *out, size_t cap, const char *name); +#if defined(JL_HAS_FILE_ROOTS) +static void ensureSaveDir(void); +#else +// SAVES/ is relative to the current directory: ensure it by its literal name. +#define ensureSaveDir() ((void)jlpSaveDirEnsure(SAVE_DIR)) +#endif -// Join SAVE_DIR_PREFIX + name into `out`. Returns false if it would overflow. +// Join SAVE_DIR_PREFIX + name into `out` (under the write root on a port +// with run-time roots, fileRoot.h). Returns false if it would overflow. static bool buildSavePath(char *out, size_t cap, const char *name) { +#if defined(JL_HAS_FILE_ROOTS) + return fileRootJoin(out, cap, jlpWriteRoot(), SAVE_DIR_PREFIX, name); +#else int written; written = snprintf(out, cap, SAVE_DIR_PREFIX "%s", name); return written >= 0 && (size_t)written < cap; +#endif } +#if defined(JL_HAS_FILE_ROOTS) +// Create SAVES/ under the write root. Advisory, like the literal form above. +static void ensureSaveDir(void) { + char dir[SAVE_PATH_MAX]; + + if (fileRootJoin(dir, sizeof(dir), jlpWriteRoot(), SAVE_DIR, "")) { + (void)jlpSaveDirEnsure(dir); + } +} +#endif + + // ----- Public API (alphabetical) ----- uint32_t jlDiskFree(void) { @@ -86,7 +110,7 @@ FILE *jlSaveOpen(const char *name, const char *mode) { // the directory cannot be made, fopen below simply fails and the caller // sees NULL. if (strchr(mode, 'w') != NULL || strchr(mode, 'a') != NULL) { - (void)jlpSaveDirEnsure(SAVE_DIR); + ensureSaveDir(); } if (!buildSavePath(path, sizeof(path), name)) { return NULL; diff --git a/src/core/sprite.c b/src/core/sprite.c index 23c88a4..163c02c 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -697,7 +697,7 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { if (!COMPILED_SPRITE_WRITES_PLANES) { jlpSpriteDrawPlanes(s, sp, x, y); } -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_CPU_X86) // P7-1 lever b: inline the dominant h==16 stage mark to drop // the spriteMarkDirty call boundary (see the restore arm in // jlSpriteRestoreUnder). Other heights / non-stage draws keep @@ -725,7 +725,7 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { // fully-validated jlSpriteDraw for uncompiled sprites and on DOS / // generic. void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_CPU_X86) if (sp->slot != NULL) { uint8_t shift; uint16_t drawOffset; @@ -848,7 +848,7 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); jlpSpriteDrawPlanes(s, sp, x, y); } -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_CPU_X86) // P7-1 lever b: inline the h==16 stage mark (see jlSpriteDraw). if (s == gStage && heightPx == 16) { SPRITE_MARK_STAGE_ROWS16(x, widthPx, y); @@ -881,7 +881,7 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS // fully-validated jlSpriteSaveAndDraw for uncompiled sprites and on // DOS / generic. void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) { -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_CPU_X86) ArenaSlotT *slot; slot = sp->slot; @@ -1008,7 +1008,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { return; } } -#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_DOS) +#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_CPU_X86) // Fused stage restore (P7-1 lever b): the compiled copy body // stays INLINED (spriteCompiledRestoreUnder is always-inline, // sharing the backup-field loads with the validation above), @@ -1085,7 +1085,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) { // restore+mark JSL, mirroring the safe fused arm); on DOS / generic this // is just the validated call. void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup) { -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_DOS) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_CPU_X86) jlSpriteT *sp; int16_t bx; int16_t by; diff --git a/src/core/tile.c b/src/core/tile.c index d502207..ce62781 100644 --- a/src/core/tile.c +++ b/src/core/tile.c @@ -305,7 +305,7 @@ void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uin #elif defined(jlpTileMapPaste) // Whole-map engine (NATIVE-PERF Phase 4 ST / Phase 6 W3 seam): // port.h aliases jlpTileMapPaste to the port's engine (ST - // stTileMapPaste, DOS dosTileMapPaste), which hoists the setup -- + // stTileMapPaste, DOS/DVX x86TileMapPaste), which hoists the setup -- // portData/pixels check, row-base derive, group or dword walk -- // out of the per-cell loop the generic arm below pays. One arm, // one mark stanza, however many ports bring engines. diff --git a/src/dos/input.c b/src/dos/input.c index 49e397f..7b1a704 100644 --- a/src/dos/input.c +++ b/src/dos/input.c @@ -45,6 +45,7 @@ #include "port.h" #include "inputInternal.h" #include "joey/surface.h" +#include "scanSet1.h" // ----- Constants ----- @@ -54,7 +55,6 @@ #define SCAN_BREAK_BIT 0x80 #define SCAN_CODE_MASK 0x7F #define SCAN_EXTENDED 0xE0 -#define SCAN_TABLE_SIZE 128 #define ISR_LOCK_SIZE 4096 // Caps/num-lock make codes, and the BIOS keyboard shift-flag byte in @@ -141,73 +141,11 @@ static int8_t scaleAxis(uint16_t v); // ----- Module state ----- -static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = { - [0x01] = KEY_ESCAPE, - [0x02] = KEY_1, - [0x03] = KEY_2, - [0x04] = KEY_3, - [0x05] = KEY_4, - [0x06] = KEY_5, - [0x07] = KEY_6, - [0x08] = KEY_7, - [0x09] = KEY_8, - [0x0A] = KEY_9, - [0x0B] = KEY_0, - [0x0E] = KEY_BACKSPACE, - [0x0F] = KEY_TAB, - [0x10] = KEY_Q, - [0x11] = KEY_W, - [0x12] = KEY_E, - [0x13] = KEY_R, - [0x14] = KEY_T, - [0x15] = KEY_Y, - [0x16] = KEY_U, - [0x17] = KEY_I, - [0x18] = KEY_O, - [0x19] = KEY_P, - [0x1C] = KEY_RETURN, - [0x1D] = KEY_LCTRL, - [0x1E] = KEY_A, - [0x1F] = KEY_S, - [0x20] = KEY_D, - [0x21] = KEY_F, - [0x22] = KEY_G, - [0x23] = KEY_H, - [0x24] = KEY_J, - [0x25] = KEY_K, - [0x26] = KEY_L, - [0x2A] = KEY_LSHIFT, - [0x2C] = KEY_Z, - [0x2D] = KEY_X, - [0x2E] = KEY_C, - [0x2F] = KEY_V, - [0x30] = KEY_B, - [0x31] = KEY_N, - [0x32] = KEY_M, - [0x36] = KEY_RSHIFT, - [0x38] = KEY_LALT, - [0x39] = KEY_SPACE, - [0x3B] = KEY_F1, - [0x3C] = KEY_F2, - [0x3D] = KEY_F3, - [0x3E] = KEY_F4, - [0x3F] = KEY_F5, - [0x40] = KEY_F6, - [0x41] = KEY_F7, - [0x42] = KEY_F8, - [0x43] = KEY_F9, - [0x44] = KEY_F10, - [0x48] = KEY_UP, - [0x4B] = KEY_LEFT, - [0x4D] = KEY_RIGHT, - [0x50] = KEY_DOWN, -}; - // US-layout set-1 scancode -> ASCII, normal and shifted. Only make // codes reach these; slots left at 0 produce no character. Letters // are lowercase here -- the ISR flips to the shifted table when the // effective shift (shift XOR caps, letters only) is active. -static const uint8_t gScanAsciiNormal[SCAN_TABLE_SIZE] = { +static const uint8_t gScanAsciiNormal[SCAN_SET1_CODES] = { [0x01] = JL_CHAR_ESCAPE, [0x02] = '1', [0x03] = '2', [0x04] = '3', [0x05] = '4', [0x06] = '5', [0x07] = '6', [0x08] = '7', [0x09] = '8', @@ -228,7 +166,7 @@ static const uint8_t gScanAsciiNormal[SCAN_TABLE_SIZE] = { [0x4A] = '-', [0x4E] = '+', }; -static const uint8_t gScanAsciiShift[SCAN_TABLE_SIZE] = { +static const uint8_t gScanAsciiShift[SCAN_SET1_CODES] = { [0x01] = JL_CHAR_ESCAPE, [0x02] = '!', [0x03] = '@', [0x04] = '#', [0x05] = '$', [0x06] = '%', [0x07] = '^', [0x08] = '&', [0x09] = '*', @@ -328,7 +266,7 @@ static void keyboardIsr(void) { return; } - key = gScanToKey[code]; + key = gScanSet1ToKey[code]; if (key != KEY_NONE) { gIsrState[key] = !isBreak; } @@ -559,20 +497,20 @@ void jlpInputInit(void) { gIsrCharTail = 0; _go32_dpmi_lock_code(keyboardIsr, ISR_LOCK_SIZE); - _go32_dpmi_lock_data((void *)gScanToKey, sizeof(gScanToKey)); - _go32_dpmi_lock_data((void *)gIsrState, sizeof(gIsrState)); - _go32_dpmi_lock_data((void *)gScanAsciiNormal, sizeof(gScanAsciiNormal)); - _go32_dpmi_lock_data((void *)gScanAsciiShift, sizeof(gScanAsciiShift)); - _go32_dpmi_lock_data((void *)gKeypadAscii, sizeof(gKeypadAscii)); - _go32_dpmi_lock_data((void *)gIsrCharQueue, sizeof(gIsrCharQueue)); - _go32_dpmi_lock_data((void *)&gIsrExtended, sizeof(gIsrExtended)); - _go32_dpmi_lock_data((void *)&gIsrE1Skip, sizeof(gIsrE1Skip)); - _go32_dpmi_lock_data((void *)&gIsrCapsLock, sizeof(gIsrCapsLock)); - _go32_dpmi_lock_data((void *)&gIsrCapsHeld, sizeof(gIsrCapsHeld)); - _go32_dpmi_lock_data((void *)&gIsrNumLock, sizeof(gIsrNumLock)); - _go32_dpmi_lock_data((void *)&gIsrNumHeld, sizeof(gIsrNumHeld)); - _go32_dpmi_lock_data((void *)&gIsrCharHead, sizeof(gIsrCharHead)); - _go32_dpmi_lock_data((void *)&gIsrCharTail, sizeof(gIsrCharTail)); + _go32_dpmi_lock_data((void *)gScanSet1ToKey, sizeof(gScanSet1ToKey)); + _go32_dpmi_lock_data((void *)gIsrState, sizeof(gIsrState)); + _go32_dpmi_lock_data((void *)gScanAsciiNormal, sizeof(gScanAsciiNormal)); + _go32_dpmi_lock_data((void *)gScanAsciiShift, sizeof(gScanAsciiShift)); + _go32_dpmi_lock_data((void *)gKeypadAscii, sizeof(gKeypadAscii)); + _go32_dpmi_lock_data((void *)gIsrCharQueue, sizeof(gIsrCharQueue)); + _go32_dpmi_lock_data((void *)&gIsrExtended, sizeof(gIsrExtended)); + _go32_dpmi_lock_data((void *)&gIsrE1Skip, sizeof(gIsrE1Skip)); + _go32_dpmi_lock_data((void *)&gIsrCapsLock, sizeof(gIsrCapsLock)); + _go32_dpmi_lock_data((void *)&gIsrCapsHeld, sizeof(gIsrCapsHeld)); + _go32_dpmi_lock_data((void *)&gIsrNumLock, sizeof(gIsrNumLock)); + _go32_dpmi_lock_data((void *)&gIsrNumHeld, sizeof(gIsrNumHeld)); + _go32_dpmi_lock_data((void *)&gIsrCharHead, sizeof(gIsrCharHead)); + _go32_dpmi_lock_data((void *)&gIsrCharTail, sizeof(gIsrCharTail)); _go32_dpmi_get_protected_mode_interrupt_vector(9, &gOldHandler); diff --git a/src/dvx/audio.c b/src/dvx/audio.c new file mode 100644 index 0000000..6c84ba7 --- /dev/null +++ b/src/dvx/audio.c @@ -0,0 +1,491 @@ +// DVX audio HAL: JoeyLib's sound through DVX's sound library (audio.lib) +// and music library (music.lib), which own the sound card for every +// program on the desktop and mix them all together. +// +// MOD music.lib (libxmp-lite inside DVX: no second copy here), +// loaded from memory; music.lib plays it through audio.lib. +// SFX five slots, each at most one sound: a one-shot is an +// audio.lib sample made from the program's signed 8-bit PCM +// (the top bit flipped: DVX's 8-bit samples are unsigned); a +// stream is an audio.lib pull stream whose fill calls the +// program's, flipping the same bit. JoeyLib's end of stream +// (a fill returning 0) closes the stream once what it queued +// has played. Pan sticks to the slot, as on every port. +// Tone, audio.lib tones that play until changed: jlAudioTone a +// voices, square, the three jlAudioVoice voices squares, jlAudioNoise +// noise a noise tone. DVX puts squares on the FM chip when there is +// one and mixes them otherwise, and keeps noise in the mixer. +// +// None of this may run inside an interrupt (audio.lib's rule), so +// jlAudioTickRegister's callback runs in program time: a DVX timer counts +// the ticks at the asked rate, and jlAudioFrameTick delivers the ones that +// came due, in order. A callback can therefore do anything; what DVX loses +// against DOS is the sub-frame placement of what it does. jlAudioCritical* +// stay the generic no-ops: nothing the callback touches can be interrupted +// by it. +// +// jlAudioFrameTick is also where DVX's music and streams are fed +// (musicPump, which pumps audio.lib too) when the program keeps the +// desktop busy. A context that could not be opened -- DVX without a sound +// library -- leaves everything silent; DVX without a sound card still opens +// one (its null driver plays nothing). + +#include "dvxSdk.h" + +#include +#include + +#include "port.h" +#include "joey/audio.h" +#include "dvxPort.h" + +// ----- Constants ----- + +#define NO_VOICE 0 // audio.lib voice ids are > 0 +#define NO_TIMER (-1) + +// JoeyLib SFX are signed 8-bit; DVX's 8-bit PCM is unsigned. +#define PCM8_SIGN_FLIP 0x80u +#define SFX_BITS 8 +#define SFX_CHANNELS 1 +#define SFX_VOLUME AUDIO_VOLUME_MAX +// A stream's own buffer: DVX refills it every desktop frame or so. +#define STREAM_BUFFER_MS 100 + +// Tone voices at half scale: the three voices, the noise and the tone +// together stay clear of the mixer's clipping at full volume. +#define TONE_VOLUME (AUDIO_VOLUME_MAX / 2) +#define TONE_UNTIL_STOPPED 0 +// Attenuation 15 (and up) is silence (joey/audio.h). +#define ATTEN_SILENT 15u + +// jlAudioNoise pitch 0..31, spread linearly over the noise rates like the +// shared curve the other ports follow (26000 - pitch * 750 Hz, whose top +// audio.lib cannot reach): from the fastest rate a tone may have down to +// that curve's slowest. +#define NOISE_PITCH_MAX 31u +#define NOISE_CURVE_TOP_HZ 26000u +#define NOISE_CURVE_STEP_HZ 750u +#define NOISE_MAX_HZ AUDIO_TONE_MAX_HZ +#define NOISE_MIN_HZ (NOISE_CURVE_TOP_HZ - NOISE_PITCH_MAX * NOISE_CURVE_STEP_HZ) + +// Pinned for the timer interrupt: the tick counter's code (one small +// function; a page is as fine a grain as pinning has) and data. +#define TICK_ISR_LOCK_BYTES 4096u + +// ----- Types ----- + +// One SFX slot: a one-shot sample or a stream (or neither), its voice and +// its sticky pan. +typedef struct { + AudioSampleT *sample; + AudioStreamT *stream; + jlAudioStreamFillT fill; + void *fillCtx; + int32_t voice; + int32_t pan; + bool streamEnded; +} SfxSlotT; + +// One tone channel (the tone, a voice, the noise): an audio.lib tone that +// plays until changed, or NO_VOICE while silent. +typedef struct { + AudioWaveE wave; + int32_t voice; +} ToneChannelT; + +// ----- Prototypes ----- + +static AudioCtxT *audioContext(void); +static void sfxFormat(uint16_t rateHz, AudioFormatT *fmt, int32_t *pitch); +static void sfxStart(uint8_t slot, int32_t voice, uint16_t rateHz); +static void sfxStop(uint8_t slot); +static void streamEndReap(void); +static uint32_t streamFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData); +static void tickDeliver(void); +static void tickIsr(void *userData); +static void tickStop(void); +static void toneSet(ToneChannelT *channel, uint32_t hz, int32_t volume); +static int32_t voiceVolume(uint8_t atten); + +// ----- Module state ----- + +static AudioCtxT *gCtx = NULL; +static MusicT *gMusic = NULL; +static SfxSlotT gSlots[JOEY_AUDIO_SFX_SLOTS]; + +// DVX pan for each JoeyLib pan. +static const int32_t gPanToDvx[AUDIO_PAN_COUNT] = { + [AUDIO_PAN_LEFT] = DVX_PAN_LEFT, + [AUDIO_PAN_CENTER] = DVX_PAN_CENTER, + [AUDIO_PAN_RIGHT] = DVX_PAN_RIGHT +}; + +static ToneChannelT gTone = { AUDIO_WAVE_SQUARE, NO_VOICE }; +static ToneChannelT gVoices[JOEY_AUDIO_VOICES] = { + { AUDIO_WAVE_SQUARE, NO_VOICE }, + { AUDIO_WAVE_SQUARE, NO_VOICE }, + { AUDIO_WAVE_SQUARE, NO_VOICE } +}; +static ToneChannelT gNoise = { AUDIO_WAVE_NOISE, NO_VOICE }; + +// Volume for an SN76489 / AGI attenuation below silence: 2 dB per step, +// AUDIO_VOLUME_MAX * 10^(-atten / 10). +static const uint16_t gAttenVolume[ATTEN_SILENT] = { + 256, 203, 162, 128, 102, 81, 64, 51, 41, 32, 26, 20, 16, 13, 10 +}; + +// The converted copy of the last one-shot's PCM (audio.lib copies it again). +static uint8_t *gPcm = NULL; +static uint32_t gPcmSize = 0; + +// jlAudioTickRegister: the callback, its DVX timer and the ticks the timer +// has counted that are not delivered yet. +static jlAudioTickFnT gTickFn = NULL; +static int32_t gTickTimer = NO_TIMER; +static volatile uint32_t gTicksDue = 0; +static bool gTickPinned = false; + +// ----- Internal helpers (alphabetical) ----- + +// The program's audio context, opened on first use: the voice layer may be +// used without jlAudioInit, as on the other ports. +static AudioCtxT *audioContext(void) { + if (gCtx == NULL) { + gCtx = audioOpen(); + } + return gCtx; +} + + +// A one-shot's PCM format, and the pitch that makes a rate audio.lib cannot +// take play right: it is created at the nearest rate it takes and played +// slower or faster. +static void sfxFormat(uint16_t rateHz, AudioFormatT *fmt, int32_t *pitch) { + uint32_t rate; + + rate = rateHz < AUDIO_RATE_MIN ? AUDIO_RATE_MIN : (rateHz > AUDIO_RATE_MAX ? AUDIO_RATE_MAX : rateHz); + fmt->rate = rate; + fmt->bits = SFX_BITS; + fmt->channels = SFX_CHANNELS; + *pitch = (int32_t)(AUDIO_PITCH_NORMAL * (uint32_t)rateHz / rate); +} + + +// A slot's new voice: its pan, and its pitch for a rate audio.lib cannot +// take directly. +static void sfxStart(uint8_t slot, int32_t voice, uint16_t rateHz) { + AudioFormatT fmt; + int32_t pitch; + + if (voice <= 0) { + return; + } + gSlots[slot].voice = voice; + sfxFormat(rateHz, &fmt, &pitch); + if (pitch != AUDIO_PITCH_NORMAL) { + (void)audioVoiceSetPitch(gCtx, voice, pitch); + } + (void)audioVoiceSetPan(gCtx, voice, gSlots[slot].pan); +} + + +// Stop a slot: its stream closed or its sample freed (either stops the +// voice). +static void sfxStop(uint8_t slot) { + SfxSlotT *s; + + s = &gSlots[slot]; + audioStreamClose(s->stream); + audioSampleFree(s->sample); + s->stream = NULL; + s->sample = NULL; + s->fill = NULL; + s->voice = NO_VOICE; + s->streamEnded = false; +} + + +// Close the streams whose program fill has ended, once what they queued +// has played. +static void streamEndReap(void) { + uint8_t slot; + + for (slot = 0; slot < JOEY_AUDIO_SFX_SLOTS; slot++) { + if (gSlots[slot].streamEnded && audioStreamQueued(gSlots[slot].stream) == 0) { + sfxStop(slot); + } + } +} + + +// audio.lib asks a slot's stream for PCM: the program's fill writes signed +// bytes in place, which then become unsigned. Its 0 is the end of the +// stream; audio.lib's 0 only means "nothing yet", so the slot remembers. +static uint32_t streamFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) { + SfxSlotT *s; + uint8_t *pcm; + uint32_t got; + uint32_t i; + + (void)stream; + s = (SfxSlotT *)userData; + if (s->streamEnded) { + return 0; + } + got = s->fill(s->fillCtx, (int8_t *)buf, frames); + if (got == 0) { + s->streamEnded = true; + return 0; + } + got = got < frames ? got : frames; + pcm = (uint8_t *)buf; + for (i = 0; i < got; i++) { + pcm[i] ^= PCM8_SIGN_FLIP; + } + return got; +} + + +// Run the tick callback once for every tick the timer counted since last +// time. +static void tickDeliver(void) { + uint32_t state; + uint32_t ticks; + + if (gTickFn == NULL) { + return; + } + state = platformIrqSave(); + ticks = gTicksDue; + gTicksDue = 0; + platformIrqRestore(state); + while (ticks > 0 && gTickFn != NULL) { + ticks--; + gTickFn(); + } +} + + +// The DVX timer callback, inside the timer interrupt: count, nothing more. +static void tickIsr(void *userData) { + (void)userData; + gTicksDue++; +} + + +static void tickStop(void) { + if (gTickTimer != NO_TIMER) { + platformTimerRemove(gTickTimer); + gTickTimer = NO_TIMER; + } + gTickFn = NULL; + gTicksDue = 0; + if (gTickPinned) { + (void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true); + (void)platformUnlockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false); + gTickPinned = false; + } +} + + +// A tone channel at hz and volume, 0 for either being silence. A playing +// tone is changed in place; one audio.lib dropped (its voice limit) or +// never started is started. +static void toneSet(ToneChannelT *channel, uint32_t hz, int32_t volume) { + AudioCtxT *ctx; + + // Silence needs no context: a channel only has a voice when there is one. + if (hz == 0 || volume == 0) { + if (channel->voice != NO_VOICE) { + (void)audioVoiceStop(gCtx, channel->voice); + channel->voice = NO_VOICE; + } + return; + } + ctx = audioContext(); + if (ctx == NULL) { + return; + } + hz = hz < AUDIO_TONE_MIN_HZ ? AUDIO_TONE_MIN_HZ : (hz > AUDIO_TONE_MAX_HZ ? AUDIO_TONE_MAX_HZ : hz); + if (channel->voice != NO_VOICE && + audioVoiceSetFrequency(ctx, channel->voice, hz) == AUDIO_OK && + audioVoiceSetVolume(ctx, channel->voice, volume) == AUDIO_OK) { + return; + } + channel->voice = audioTone(ctx, channel->wave, hz, volume, TONE_UNTIL_STOPPED); + if (channel->voice < 0) { + channel->voice = NO_VOICE; + } +} + + +static int32_t voiceVolume(uint8_t atten) { + if (atten >= ATTEN_SILENT) { + return 0; + } + return (int32_t)gAttenVolume[atten] * TONE_VOLUME / AUDIO_VOLUME_MAX; +} + + +// ----- HAL API (alphabetical) ----- + +void jlpAudioFrameTick(void) { + tickDeliver(); + streamEndReap(); + // Feeds the music and the streams; pumps audio.lib too. + musicPump(); +} + + +bool jlpAudioInit(void) { + uint8_t slot; + + for (slot = 0; slot < JOEY_AUDIO_SFX_SLOTS; slot++) { + gSlots[slot].pan = DVX_PAN_CENTER; + } + return audioContext() != NULL; +} + + +bool jlpAudioIsPlayingMod(void) { + return gMusic != NULL && musicIsPlaying(gMusic); +} + + +void jlpAudioNoise(uint8_t pitch, uint8_t atten) { + uint32_t hz; + + hz = NOISE_MAX_HZ - (uint32_t)pitch * (NOISE_MAX_HZ - NOISE_MIN_HZ) / NOISE_PITCH_MAX; + toneSet(&gNoise, hz, voiceVolume(atten)); +} + + +void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { + jlpAudioStopMod(); + if (musicLoadMem(data, length, &gMusic) != MUSIC_OK) { + gMusic = NULL; + return; + } + (void)musicPlay(gMusic, loop); +} + + +void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { + AudioFormatT fmt; + int32_t pitch; + uint8_t *grown; + uint32_t i; + + sfxStop(slot); + if (length > gPcmSize) { + grown = (uint8_t *)realloc(gPcm, length); + if (grown == NULL) { + return; + } + gPcm = grown; + gPcmSize = length; + } + for (i = 0; i < length; i++) { + gPcm[i] = (uint8_t)(sample[i] ^ PCM8_SIGN_FLIP); + } + sfxFormat(rateHz, &fmt, &pitch); + if (audioSampleCreate(gCtx, gPcm, length, &fmt, &gSlots[slot].sample) != AUDIO_OK) { + gSlots[slot].sample = NULL; + return; + } + sfxStart(slot, audioPlay(gSlots[slot].sample, SFX_VOLUME, gSlots[slot].pan, false), rateHz); +} + + +void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { + AudioFormatT fmt; + int32_t pitch; + SfxSlotT *s; + + sfxStop(slot); + s = &gSlots[slot]; + s->fill = fill; + s->fillCtx = ctx; + sfxFormat(rateHz, &fmt, &pitch); + if (audioStreamOpen(gCtx, &fmt, STREAM_BUFFER_MS, streamFill, s, &s->stream) != AUDIO_OK) { + s->stream = NULL; + s->fill = NULL; + return; + } + sfxStart(slot, audioStreamVoice(s->stream), rateHz); +} + + +void jlpAudioSetPan(uint8_t slot, uint8_t pan) { + gSlots[slot].pan = gPanToDvx[pan]; + if (gSlots[slot].voice != NO_VOICE) { + (void)audioVoiceSetPan(gCtx, gSlots[slot].voice, gSlots[slot].pan); + } +} + + +void jlpAudioShutdown(void) { + uint8_t i; + + tickStop(); + jlpAudioStopMod(); + for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) { + sfxStop(i); + } + toneSet(&gTone, 0, 0); + toneSet(&gNoise, 0, 0); + for (i = 0; i < JOEY_AUDIO_VOICES; i++) { + toneSet(&gVoices[i], 0, 0); + } + free(gPcm); + gPcm = NULL; + gPcmSize = 0; + audioClose(gCtx); + gCtx = NULL; +} + + +void jlpAudioStopMod(void) { + musicFree(gMusic); + gMusic = NULL; +} + + +void jlpAudioStopSfx(uint8_t slot) { + sfxStop(slot); +} + + +bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { + tickStop(); + if (fn == NULL) { + return true; + } + if (platformLockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true) != 0) { + return false; + } + if (platformLockRegion((const void *)&gTicksDue, sizeof(gTicksDue), false) != 0) { + (void)platformUnlockRegion((const void *)tickIsr, TICK_ISR_LOCK_BYTES, true); + return false; + } + gTickPinned = true; + gTickFn = fn; + gTickTimer = gameTimerAdd(gDvxPortProgram->shellCtx, hz, tickIsr, NULL); + if (gTickTimer < 0) { + gTickTimer = NO_TIMER; + tickStop(); + return false; + } + return true; +} + + +void jlpAudioTone(uint16_t freqHz) { + toneSet(&gTone, freqHz, TONE_VOLUME); +} + + +void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { + toneSet(&gVoices[voice], freqHz, voiceVolume(atten)); +} diff --git a/src/dvx/dvxApp.c b/src/dvx/dvxApp.c new file mode 100644 index 0000000..37b4fc3 --- /dev/null +++ b/src/dvx/dvxApp.c @@ -0,0 +1,38 @@ +// The DVX entry of a JoeyLib program: the .app descriptor DVX reads when it +// loads the module, and appMain, which DVX runs in the program's own task. +// +// Not part of libjoey.a: make/dvx.mk compiles this file into every program, +// because the descriptor carries the program's name (DVX_PORT_APP_NAME) and +// the linker would never pull an entry point nothing calls out of an +// archive. Everything else is in the library (program.c). + +#include "dvxSdk.h" + +#include + +#include "dvxPort.h" + +#ifndef DVX_PORT_APP_NAME +#error "make/dvx.mk passes -DDVX_PORT_APP_NAME=\"Name\" for each program" +#endif + +// ----- Prototypes ----- + +int32_t appMain(DxeAppContextT *ctx); + +// ----- App descriptor ----- + +// A main-loop program: DVX gives it a task of its own, in which the JoeyLib +// program's main runs its frame loop, yielding in jlWaitVBL. +SHELL_APP_DESCRIPTOR( + .name = DVX_PORT_APP_NAME, + .hasMainLoop = true, + .multiInstance = false, + .stackSize = DVX_PORT_STACK_BYTES, + .priority = TS_PRIORITY_NORMAL +); + + +int32_t appMain(DxeAppContextT *ctx) { + return dvxPortRun(ctx); +} diff --git a/src/dvx/dvxPort.h b/src/dvx/dvxPort.h new file mode 100644 index 0000000..c32a76d --- /dev/null +++ b/src/dvx/dvxPort.h @@ -0,0 +1,61 @@ +// Shared by the files of the DVX port (src/dvx). Include after dvxSdk.h. +// +// 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. +#ifndef JOEY_DVX_PORT_H +#define JOEY_DVX_PORT_H + +#include +#include + +// The program's task stack. DVX's default (32 KB) is far below what a DOS +// program gets, and JoeyLib programs keep sizable buffers on the stack. +#define DVX_PORT_STACK_BYTES (256 * 1024) + +// ----- dvxApp.c ----- + +// The program's descriptor; its name titles the window. +extern AppDescriptorT appDescriptor; + +// ----- program.c ----- + +// The running program's DVX context (appMain's argument). +extern DxeAppContextT *gDvxPortProgram; + +// The program's atexit() and exit(): make/dvx.mk renames the calls in the +// library and the program to these. exit() returns to dvxPortRun, so the +// handlers run, and JoeyLib shuts down and closes its log, when the program +// ends, however it ends, and appMain returns as DVX expects. DVX's own +// atexit would keep the handlers until DVX itself exits, long after the +// program's code is gone. +int dvxPortAtExit(void (*fn)(void)); +void dvxPortExit(int status) __attribute__((noreturn)); + +// appMain's body: run the program's main, then end the program; returns +// main's status. +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); + +// 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); + +// ----- input.c: the window's input callbacks, installed by hal.c ----- + +void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod); +void dvxPortOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); + +#endif diff --git a/src/dvx/dvxSdk.h b/src/dvx/dvxSdk.h new file mode 100644 index 0000000..660113c --- /dev/null +++ b/src/dvx/dvxSdk.h @@ -0,0 +1,53 @@ +// The DVX SDK headers the DVX port uses, made safe to mix with JoeyLib's. +// Include it FIRST in every src/dvx file, before any JoeyLib header. +// +// The SDK #defines names JoeyLib uses as enum constants: KEY_ESCAPE, KEY_F1, +// KEY_UP ... (jlKeyE, joey/input.h) and AUDIO_PAN_LEFT / CENTER / RIGHT +// (jlAudioPanE, joey/audio.h), with DVX's own values. The port needs none of +// the SDK's key macros; its pan values are kept as the DVX_PAN_* constants +// below. All of them are then #undef-ed, so after this header every one of +// those names is JoeyLib's. +#ifndef JOEY_DVX_SDK_H +#define JOEY_DVX_SDK_H + +#include "dvxApp.h" +#include "dvxPal.h" +#include "dvxPlat.h" +#include "dvxVideo.h" +#include "dvxWm.h" +#include "shellApp.h" +#include "audio.h" +#include "game.h" +#include "joystick.h" +#include "music.h" + +// DVX's pan range, taken while its macros still stand. +enum { + DVX_PAN_LEFT = AUDIO_PAN_LEFT, + DVX_PAN_CENTER = AUDIO_PAN_CENTER, + DVX_PAN_RIGHT = AUDIO_PAN_RIGHT +}; + +#undef AUDIO_PAN_CENTER +#undef AUDIO_PAN_LEFT +#undef AUDIO_PAN_RIGHT +#undef KEY_BACKSPACE +#undef KEY_DOWN +#undef KEY_ESCAPE +#undef KEY_F1 +#undef KEY_F10 +#undef KEY_F2 +#undef KEY_F3 +#undef KEY_F4 +#undef KEY_F5 +#undef KEY_F6 +#undef KEY_F7 +#undef KEY_F8 +#undef KEY_F9 +#undef KEY_LEFT +#undef KEY_RIGHT +#undef KEY_SPACE +#undef KEY_TAB +#undef KEY_UP + +#endif diff --git a/src/dvx/hal.c b/src/dvx/hal.c new file mode 100644 index 0000000..83d6a84 --- /dev/null +++ b/src/dvx/hal.c @@ -0,0 +1,487 @@ +// DVX HAL: the stage in a window, 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 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 +// one table per palette: the desktop pixel for each of its 16 entries, the +// $0RGB channels scaled to 8 bits and packed (packColor), or on a +// 256-colour desktop the nearest desktop colour (gamePalNearest: that +// palette belongs to the whole desktop). Each row takes the table its SCB +// picks, so SCB palettes are exact at 15 bits and up. A palette change +// rebuilds its table and expands every row using it again; an SCB change, +// 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. +// +// 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. +// +// DVX multitasks cooperatively: the desktop -- the other programs, this +// program's window events (typed keys, the mouse, the close box) and the +// compositing of its window -- runs only while the program yields. A JoeyLib +// program need not call jlWaitVBL (plenty loop on jlInputPoll alone, as a +// DOS program may), so the input poll and the present yield too, at most +// once a virtual frame (dvxPortService), which keeps their cost low. + +#include "dvxSdk.h" + +#include +#include +#include + +#include "port.h" +#include "surfaceInternal.h" +#include "dvxPort.h" + +// ----- Constants ----- + +#define DVX_FRAME_HZ 70u +#define DVX_SCALE_MAX 3 +#define US_PER_MS 1000u + +// $0RGB: 4 bits per channel, widened to 8 by x17 (0xF -> 0xFF). +#define RGB12_RED_SHIFT 8 +#define RGB12_GREEN_SHIFT 4 +#define RGB12_CHANNEL_MASK 0x0Fu +#define RGB12_TO_RGB24 17u + +// Chunky stage layout: two pixels per byte, high nibble left. +#define STAGE_PIXELS_PER_BYTE 2 +#define STAGE_PIXELS_PER_WORD (SURFACE_WIDTH / SURFACE_WORDS_PER_ROW) +#define NIBBLE_BITS 4 +#define NIBBLE_MASK 0x0Fu + +// Row masks for presentRows: bit p = every row using palette p. +#define PALETTES_NONE 0x0000u +#define PALETTES_ALL 0xFFFFu + +// Game menu commands. +#define CMD_GAME_HELP 100 +#define CMD_GAME_EXIT 101 + +// ----- 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); + +// ----- Module state ----- + +WindowT *gDvxPortWindow = NULL; +int32_t gDvxPortScale = 1; + +static GameFramePacerT gPacer; +static bool gClockStarted = false; +static bool gCloseRequested = false; +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. +static uint32_t gLut[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; +static int32_t gLutBits = 0; +static uint8_t gLutDesktopPalette[PAL_ENTRIES * PAL_CHANNELS]; + +// ----- Internal helpers (alphabetical) ----- + +// The virtual frame now running: frame n starts n / DVX_FRAME_HZ seconds +// after the clock's zero. +static uint64_t clockFrame(void) { + return clockMicros() * DVX_FRAME_HZ / PLATFORM_US_PER_SEC; +} + + +// Microseconds since the program first asked for the time. The pacer's +// start is the clock's zero, so the frame count and jlWaitVBL agree. +static uint64_t clockMicros(void) { + if (!gClockStarted) { + gameFrameInit(&gPacer, DVX_FRAME_HZ); + gClockStarted = true; + } + return platformMicros() - gPacer.startUs; +} + + +// 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) { + 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; + + switch (bytesPerPixel) { + case 1: { + uint8_t *d = row; + + 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; + } + break; + } + + case 2: { + uint16_t *d = (uint16_t *)row; + uint16_t left; + uint16_t right; + + 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++) { + *d++ = left; + } + for (s = 0; s < scale; s++) { + *d++ = right; + } + } + break; + } + + default: { + uint32_t *d = (uint32_t *)row; + uint32_t left; + uint32_t right; + + for (i = 0; i < pairs; i++) { + left = lut[in[i] >> NIBBLE_BITS]; + right = lut[in[i] & NIBBLE_MASK]; + for (s = 0; s < scale; s++) { + *d++ = left; + } + for (s = 0; s < scale; s++) { + *d++ = right; + } + } + break; + } + } + + for (s = 1; s < scale; s++) { + memcpy(row + s * pitch, row, (size_t)((x1 - x0) * scale * bytesPerPixel)); + } +} + + +// Rebuild the tables of the palettes whose bits are set. +static void lutBuild(const jlSurfaceT *src, uint16_t palettes) { + const DisplayT *d; + uint16_t rgb; + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t p; + uint8_t c; + + d = dvxGetDisplay(gDvxPortProgram->shellCtx); + 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); + } + } +} + + +// 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. +static bool lutSync(const jlSurfaceT *src) { + const DisplayT *d; + + d = dvxGetDisplay(gDvxPortProgram->shellCtx); + if (d->format.bitsPerPixel == gLutBits && (d->palette == NULL || memcmp(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette)) == 0)) { + return false; + } + gLutBits = d->format.bitsPerPixel; + if (d->palette != NULL) { + memcpy(gLutDesktopPalette, d->palette, sizeof(gLutDesktopPalette)); + } + lutBuild(src, PALETTES_ALL); + return true; +} + + +static void onClose(WindowT *win) { + (void)win; + gCloseRequested = true; +} + + +static void onMenu(WindowT *win, int32_t menuId) { + (void)win; + switch (menuId) { + case CMD_GAME_HELP: + shellOpenHelp(gDvxPortProgram->shellCtx, gDvxPortProgram->helpFile); + break; + + case CMD_GAME_EXIT: + gCloseRequested = true; + break; + + default: + break; + } +} + + +// 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. +static void onPaint(WindowT *win, RectT *dirty) { + (void)win; + (void)dirty; + if (gStage == NULL) { + return; + } + (void)lutSync(gStage); + presentRows(gStage, PALETTES_ALL, false); +} + + +// 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; + + if (gDvxPortWindow == NULL || gDvxPortWindow->contentBuf == 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++) { + 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); + } + runY0 = -1; + continue; + } else { + // Clamp a band past the row end, as the DOS present does. + maxWord = gStageMaxWord[y] < SURFACE_WORDS_PER_ROW ? gStageMaxWord[y] : (uint8_t)(SURFACE_WORDS_PER_ROW - 1); + x0 = gStageMinWord[y] * STAGE_PIXELS_PER_WORD; + x1 = (maxWord + 1) * STAGE_PIXELS_PER_WORD; + } + x1 = x1 < cols ? x1 : cols; + if (x0 >= x1) { + continue; + } + expandRow(src, y, x0, x1); + gPresentCopiedBytes += (uint32_t)(x1 - x0); + if (runY0 < 0) { + runY0 = y; + runX0 = x0; + runX1 = x1; + } else { + runX0 = x0 < runX0 ? x0 : runX0; + runX1 = x1 > runX1 ? x1 : runX1; + } + } + if (runY0 >= 0 && invalidate) { + dvxInvalidateRect(ac, gDvxPortWindow, runX0 * scale, runY0 * scale, (runX1 - runX0) * scale, (y - runY0) * scale); + } + gPresentScannedRows += SURFACE_HEIGHT; +} + + +// 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) { + int32_t frameW; + int32_t frameH; + int32_t scaleW; + int32_t scaleH; + int32_t wantW; + int32_t wantH; + WindowT *win; + 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; + + win = dvxCreateWindowCentered(ac, appDescriptor.name, wantW + frameW, wantH + frameH, false); + if (win == NULL) { + return false; + } + win->gameInput = true; + win->onClose = onClose; + win->onKey = dvxPortOnKey; + win->onMenu = onMenu; + win->onMouse = dvxPortOnMouse; + win->onPaint = onPaint; + + bar = wmAddMenuBar(win); + game = bar != NULL ? wmAddMenu(bar, "&Game") : NULL; + if (game != NULL) { + wmAddMenuItem(game, "&Help", CMD_GAME_HELP); + wmAddMenuSeparator(game); + wmAddMenuItem(game, "E&xit", CMD_GAME_EXIT); + } + + // The frame was sized from the chrome constants; make the content area + // exactly the scaled stage whatever the menu bar took. + if (win->contentW != wantW || win->contentH != wantH) { + 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); + return true; +} + + +// ----- Port API (alphabetical) ----- + +void dvxPortEndIfClosed(void) { + if (gCloseRequested) { + dvxPortExit(0); + } +} + + +void dvxPortService(void) { + uint64_t frame; + + frame = clockFrame(); + if (frame != gYieldFrame) { + gYieldFrame = frame; + tsYield(); + } + dvxPortEndIfClosed(); +} + + +// ----- HAL API (alphabetical) ----- + +uint16_t jlpFrameCount(void) { + return (uint16_t)clockFrame(); +} + + +uint16_t jlpFrameHz(void) { + return DVX_FRAME_HZ; +} + + +bool jlpInit(const jlConfigT *config) { + (void)config; + gCloseRequested = false; + gLutBits = 0; + (void)clockMicros(); + return windowCreate(gDvxPortProgram->shellCtx); +} + + +uint32_t jlpMillisElapsed(void) { + return (uint32_t)(clockMicros() / US_PER_MS); +} + + +void jlpPresent(const jlSurfaceT *src) { + uint16_t palettes; + + if (src == NULL || gDvxPortWindow == NULL) { + return; + } + palettes = PALETTES_NONE; + if (lutSync(src)) { + palettes = PALETTES_ALL; + } else if (gStagePaletteDirty) { + lutBuild(src, gStagePaletteDirtyMask); + palettes = gStagePaletteDirtyMask; + } + // An SCB change moves rows to other tables: expand them all again. + if (gStageScbDirty) { + palettes = PALETTES_ALL; + } + gStagePaletteDirty = false; + gStagePaletteDirtyMask = PALETTES_NONE; + gStageScbDirty = false; + presentRows(src, palettes, true); + dvxPortService(); +} + + +void jlpShutdown(void) { + if (gDvxPortWindow != NULL) { + dvxDestroyWindow(gDvxPortProgram->shellCtx, gDvxPortWindow); + gDvxPortWindow = NULL; + } +} + + +// 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. +void jlpWaitVBL(void) { + gPacer.frame = clockFrame(); + (void)gameFrameWait(&gPacer); + gYieldFrame = gPacer.frame; + dvxPortEndIfClosed(); +} diff --git a/src/dvx/input.c b/src/dvx/input.c new file mode 100644 index 0000000..ef8ae76 --- /dev/null +++ b/src/dvx/input.c @@ -0,0 +1,205 @@ +// DVX input: keyboard, typed characters, mouse and joysticks. +// +// Keyboard: DVX keeps a table of held keys by raw PC scan code (set 1, plus +// KEY_RAW_EXT for the keys sent behind 0xE0) from its INT 9 chain. The poll +// copies it once and maps it through the set-1 table shared with DOS +// (src/x86/scanSet1.h); a key is down when either of its codes is, so the +// grey arrows are the arrows and right Ctrl is Ctrl, as on DOS. Every key +// reads up while the window does not have the focus. +// +// Typed characters come from the window's onKey, with Shift and the DVX +// 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. +// +// 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. +// +// Joysticks: DVX's joystick library, calibrated in DVX's Control Panel: +// axes -32768..32767 scaled to -127..127, real analog where DOS reads the +// BIOS digitally. DVX's keyboard joystick (keys standing in when no +// joystick is plugged in) is left out: JoeyLib then reports no joystick, as +// every port does, and programs read those keys themselves. + +#include "dvxSdk.h" + +#include + +#include "port.h" +#include "inputInternal.h" +#include "scanSet1.h" +#include "dvxPort.h" + +// ----- Constants ----- + +// DVX axes (-32768..32767) to JoeyLib's (-127..127): >> 8, then -128 up to -127. +#define JOY_AXIS_SHIFT 8 +#define JOY_AXIS_X 0 +#define JOY_AXIS_Y 1 +#define JOY_NO_HANDLE (-1) + +// Modifiers that make a key a command rather than text. +#define CHORD_MODIFIERS (KEY_MOD_CTRL | KEY_MOD_ALT) + +// ----- Prototypes ----- + +static int8_t joyAxis(int32_t value); +static void joystickClose(void); +static void joystickOpen(void); +static void joystickPoll(void); + +// ----- Module state ----- + +static int32_t gJoyHandle[JOYSTICK_COUNT]; +static int16_t gNextMouseX = 0; +static int16_t gNextMouseY = 0; +static int32_t gNextMouseButtons = 0; + +// ----- Internal helpers (alphabetical) ----- + +static int8_t joyAxis(int32_t value) { + value >>= JOY_AXIS_SHIFT; + return (int8_t)(value < JOYSTICK_AXIS_MIN ? JOYSTICK_AXIS_MIN : value); +} + + +static void joystickClose(void) { + uint8_t js; + + for (js = 0; js < JOYSTICK_COUNT; js++) { + if (gJoyHandle[js] != JOY_NO_HANDLE) { + joyClose(gJoyHandle[js]); + gJoyHandle[js] = JOY_NO_HANDLE; + } + } +} + + +// JoeyLib's two sticks are DVX's first two devices that are real +// joysticks, not the keyboard joystick. +static void joystickOpen(void) { + JoyInfoT info; + int32_t count; + int32_t device; + uint8_t js; + + for (js = 0; js < JOYSTICK_COUNT; js++) { + gJoyHandle[js] = JOY_NO_HANDLE; + } + count = joyCount(); + js = 0; + for (device = 0; device < count && js < JOYSTICK_COUNT; device++) { + if (!joyGetInfo(device, &info) || (info.caps & JOY_CAP_KEYBOARD) != 0) { + continue; + } + gJoyHandle[js] = joyOpen(gDvxPortProgram->shellCtx, device); + if (gJoyHandle[js] != JOY_NO_HANDLE) { + js++; + } + } +} + + +static void joystickPoll(void) { + JoyStateT st; + uint8_t js; + uint8_t b; + + for (js = 0; js < JOYSTICK_COUNT; js++) { + if (gJoyHandle[js] == JOY_NO_HANDLE || !joyRead(gJoyHandle[js], &st) || !st.connected) { + gJoyConnected[js] = false; + gJoyAxisX[js] = 0; + gJoyAxisY[js] = 0; + memset(gJoyButtonState[js], 0, sizeof(gJoyButtonState[js])); + continue; + } + gJoyConnected[js] = true; + gJoyAxisX[js] = joyAxis(st.axes[JOY_AXIS_X]); + gJoyAxisY[js] = joyAxis(st.axes[JOY_AXIS_Y]); + for (b = 0; b < JOY_BUTTON_COUNT; b++) { + gJoyButtonState[js][b] = (st.buttons & (1u << b)) != 0; + } + } +} + + +// ----- Port API (alphabetical) ----- + +void dvxPortOnKey(WindowT *win, int32_t key, int32_t mod) { + (void)win; + if ((mod & CHORD_MODIFIERS) != 0) { + return; + } + // Forward delete is DVX's extended Delete key; every other character is + // its own code, and jlInputCharPush drops what JoeyLib does not carry. + if (key == KEY_DELETE) { + jlInputCharPush(JL_CHAR_DELETE); + } else if (key < KEY_EXT_FLAG) { + jlInputCharPush((uint8_t)key); + } +} + + +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)); + gNextMouseButtons = buttons; +} + + +// ----- HAL API (alphabetical) ----- + +void jlpInputInit(void) { + memset(gKeyState, 0, sizeof(gKeyState)); + memset(gKeyPrev, 0, sizeof(gKeyPrev)); + // DVX always has a mouse; it starts in the middle of the stage. + gNextMouseX = SURFACE_WIDTH / 2; + gNextMouseY = SURFACE_HEIGHT / 2; + gNextMouseButtons = 0; + gMouseAttached = true; + joystickOpen(); +} + + +void jlpInputPoll(void) { + uint8_t held[KEY_RAW_COUNT]; + uint8_t scan; + uint8_t key; + + dvxPortService(); + if (gDvxPortWindow != NULL && gDvxPortWindow->focused) { + platformKeyStateGet(held); + } else { + memset(held, 0, sizeof(held)); + } + for (scan = 0; scan < SCAN_SET1_CODES; scan++) { + key = gScanSet1ToKey[scan]; + if (key != KEY_NONE) { + gKeyState[key] = (held[scan] | held[scan | KEY_RAW_EXT]) != 0; + } + } + + gMouseX = gNextMouseX; + gMouseY = gNextMouseY; + gMouseButtonState[MOUSE_BUTTON_LEFT] = (gNextMouseButtons & MOUSE_LEFT) != 0; + gMouseButtonState[MOUSE_BUTTON_RIGHT] = (gNextMouseButtons & MOUSE_RIGHT) != 0; + gMouseButtonState[MOUSE_BUTTON_MIDDLE] = (gNextMouseButtons & MOUSE_MIDDLE) != 0; + + joystickPoll(); +} + + +void jlpInputShutdown(void) { + joystickClose(); +} + + +// DVX's joysticks are calibrated in its Control Panel: nothing to reset. +void jlpJoystickReset(jlJoystickE js) { + (void)js; +} diff --git a/src/dvx/program.c b/src/dvx/program.c new file mode 100644 index 0000000..026cf0a --- /dev/null +++ b/src/dvx/program.c @@ -0,0 +1,168 @@ +// Running and ending a JoeyLib program on DVX, and the folders its files +// live in. +// +// dvxPortRun (appMain, in the program's own DVX task) records the program's +// DVX context, then calls the program's main -- compiled as jlGameMain +// (make/dvx.mk: -Dmain=jlGameMain) -- with argv[0] the .app's path and the +// launch arguments after it. A program with `int main(void)` simply ignores +// them. The program ends the same way whether main returns, it calls exit(), +// or the user closes its window (hal.c): exit() jumps back to dvxPortRun, so +// appMain always returns, the end DVX expects of a program. Then its atexit +// handlers run, then jlShutdown (nothing to do if the program already shut +// down), then the log is closed, since DVX -- which owns the file handle -- +// lives on. +// +// DVX shares one current directory between all its programs, so the file +// roots (fileRoot.h) come from the context instead: DATA/ beside the .app +// (appDir), SAVES/ and joeylog.txt in the program's own settings folder +// (configDir, CONFIG\ plus the .app's folder), which DVX keeps apart from the +// program files so a reinstall never overwrites them. + +#include "dvxSdk.h" + +#include +#include +#include + +#include "joey/audio.h" +#include "joey/core.h" +#include "dvxPort.h" +#include "fileRoot.h" + +// ----- Constants ----- + +// atexit handlers a program may register: the C standard's minimum. +#define ATEXIT_MAX 32 +// argv slots: argv[0] plus the launch arguments (the rest are dropped). +#define ARGV_MAX 16 +#define ARG_SEPARATOR ' ' +// A root: a DVX path and the separator after it (sizeof counts the NUL). +#define ROOT_MAX (DVX_MAX_PATH + sizeof(DVX_PATH_SEP)) + +// ----- Prototypes ----- + +// The program's main, as make/dvx.mk renames it (-Dmain=jlGameMain). +int jlGameMain(int argc, char **argv); + +static int argsSplit(char *args, char **argv, int max); +static void programEnd(void); +static void rootSet(char *out, size_t cap, const char *dir); + +// ----- Module state ----- + +DxeAppContextT *gDvxPortProgram = NULL; + +static void (*gAtExit[ATEXIT_MAX])(void); +static int gAtExitCount = 0; +static jmp_buf gEndJump; // back to dvxPortRun from exit() +static int gEndStatus = 0; // main's return or exit()'s status +static char gArgs[SHELL_ARGS_MAX]; +static char gReadRoot[ROOT_MAX]; +static char gWriteRoot[ROOT_MAX]; +static bool gWriteRootMade = false; + +// ----- Internal helpers (alphabetical) ----- + +// Split args in place at spaces into at most max words; returns the count. +static int argsSplit(char *args, char **argv, int max) { + int count; + char *p; + + count = 0; + p = args; + while (*p != '\0' && count < max) { + while (*p == ARG_SEPARATOR) { + p++; + } + if (*p == '\0') { + break; + } + argv[count] = p; + count++; + while (*p != '\0' && *p != ARG_SEPARATOR) { + p++; + } + if (*p == ARG_SEPARATOR) { + *p = '\0'; + p++; + } + } + return count; +} + + +// The end of the program, however it ends: atexit handlers newest first +// (as exit() runs them), JoeyLib -- its sound too, whose timer DVX would +// otherwise keep counting -- then the log. +static void programEnd(void) { + while (gAtExitCount > 0) { + gAtExitCount--; + gAtExit[gAtExitCount](); + } + jlAudioShutdown(); + jlShutdown(); + jlLogClose(); +} + + +// A root is the folder as a prefix: out = dir + separator. +static void rootSet(char *out, size_t cap, const char *dir) { + (void)snprintf(out, cap, "%s" DVX_PATH_SEP, dir); +} + + +// ----- Port API (alphabetical) ----- + +int dvxPortAtExit(void (*fn)(void)) { + if (fn == NULL || gAtExitCount == ATEXIT_MAX) { + return -1; + } + gAtExit[gAtExitCount] = fn; + gAtExitCount++; + return 0; +} + + +void dvxPortExit(int status) { + gEndStatus = status; + longjmp(gEndJump, 1); +} + + +int32_t dvxPortRun(DxeAppContextT *ctx) { + char *argv[ARGV_MAX + 1]; + int argc; + + gDvxPortProgram = ctx; + gAtExitCount = 0; + gWriteRootMade = false; + rootSet(gReadRoot, sizeof(gReadRoot), ctx->appDir); + rootSet(gWriteRoot, sizeof(gWriteRoot), ctx->configDir); + (void)snprintf(gArgs, sizeof(gArgs), "%s", ctx->args); + argv[0] = ctx->appPath; + argc = 1 + argsSplit(gArgs, &argv[1], ARGV_MAX - 1); + argv[argc] = NULL; + + if (setjmp(gEndJump) == 0) { + gEndStatus = jlGameMain(argc, argv); + } + programEnd(); + return gEndStatus; +} + + +// ----- HAL API (alphabetical) ----- + +const char *jlpReadRoot(void) { + return gReadRoot; +} + + +// The settings folder is made on first use: a program that never writes +// leaves no folder behind. +const char *jlpWriteRoot(void) { + if (!gWriteRootMade) { + gWriteRootMade = shellEnsureConfigDir(gDvxPortProgram) == 0; + } + return gWriteRoot; +} diff --git a/src/dvx/save.c b/src/dvx/save.c new file mode 100644 index 0000000..59f9e3d --- /dev/null +++ b/src/dvx/save.c @@ -0,0 +1,39 @@ +// DVX save-file + disk-space HAL. +// +// SAVES/ lives in the program's settings folder (program.c's write root); +// DVX's platform layer makes it, parents and all, and measures the disk it +// is on. Delete is stdio remove(). + +#include "dvxSdk.h" + +#include +#include + +#include "joey/file.h" +#include "port.h" +#include "fileRoot.h" +#include "dvxPort.h" + +// jlDiskFree saturates here: FAT16 disks reach 2 GB, and FAT32 more. +#define DISK_FREE_MAX 0xFFFFFFFFu + + +uint32_t jlpDiskFree(void) { + uint64_t totalBytes; + uint64_t freeBytes; + + if (!platformDiskSpace(jlpWriteRoot(), &totalBytes, &freeBytes)) { + return 0u; + } + return freeBytes > DISK_FREE_MAX ? DISK_FREE_MAX : (uint32_t)freeBytes; +} + + +bool jlpSaveDelete(const char *path) { + return remove(path) == 0; +} + + +bool jlpSaveDirEnsure(const char *dir) { + return platformMkdirRecursive(dir) == 0; +} diff --git a/src/dvx/serial.c b/src/dvx/serial.c new file mode 100644 index 0000000..cb63385 --- /dev/null +++ b/src/dvx/serial.c @@ -0,0 +1,151 @@ +// DVX serial HAL: a COM port through DVX's serial library (rs232), which +// owns the UARTs for every program on the desktop -- one owner per port, so +// a port another program has open is refused. Its interrupt fills a receive +// ring in the background; the calls here only move bytes. +// +// 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. +// +// 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 +// program that only talks serial would otherwise freeze DVX. + +#include "dvxSdk.h" +#include "rs232/rs232.h" + +#include + +#include "joey/serial.h" +#include "port.h" +#include "dvxPort.h" + +// ----- Constants ----- + +#define NO_PORT (-1) +#define COM_FIRST 1u // jlSerialConfigT.unit numbers COM ports from 1 + +// ----- Prototypes ----- + +static int32_t comForDevice(jlSerialDeviceE device, const jlSerialConfigT *config); + +// ----- Module state ----- + +static int32_t gCom = NO_PORT; + +// rs232 parity letters and handshakes for JoeyLib's. +static const char gParity[] = { + [JL_SERIAL_PARITY_NONE] = 'N', + [JL_SERIAL_PARITY_ODD] = 'O', + [JL_SERIAL_PARITY_EVEN] = 'E' +}; +static const int32_t gHandshake[] = { + [JL_SERIAL_FLOW_NONE] = RS232_HANDSHAKE_NONE, + [JL_SERIAL_FLOW_RTSCTS] = RS232_HANDSHAKE_RTSCTS, + [JL_SERIAL_FLOW_XONXOFF] = RS232_HANDSHAKE_XONXOFF +}; + +// ----- Internal helpers ----- + +static int32_t comForDevice(jlSerialDeviceE device, const jlSerialConfigT *config) { + if (device != JL_SERIAL_SLOT) { + return RS232_COM1; + } + if (config->unit < COM_FIRST || config->unit >= COM_FIRST + RS232_NUM_PORTS) { + return NO_PORT; + } + return (int32_t)(config->unit - COM_FIRST); +} + + +// ----- HAL API (alphabetical) ----- + +uint16_t jlpSerialAvailable(void) { + int32_t n; + + if (gCom == NO_PORT) { + return 0u; + } + n = rs232GetRxBuffered(gCom); + return n > 0 ? (uint16_t)n : 0u; +} + + +void jlpSerialClose(void) { + if (gCom == NO_PORT) { + return; + } + (void)rs232Close(gCom); + gCom = NO_PORT; +} + + +void jlpSerialFlush(void) { + if (gCom != NO_PORT) { + (void)rs232ClearRxBuffer(gCom); + } +} + + +bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { + int32_t com; + + if (gCom != NO_PORT || config->parity > JL_SERIAL_PARITY_EVEN || config->flow > JL_SERIAL_FLOW_XONXOFF) { + return false; + } + com = comForDevice(device, config); + if (com == NO_PORT) { + return false; + } + if (rs232Open(com, (int32_t)config->baud, config->dataBits, gParity[config->parity], config->stopBits, gHandshake[config->flow]) != RS232_SUCCESS) { + return false; + } + gCom = com; + return true; +} + + +void jlpSerialPoll(void) { + dvxPortService(); +} + + +uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { + int32_t n; + + if (gCom == NO_PORT) { + return 0u; + } + n = rs232Read(gCom, (char *)buf, max); + return n > 0 ? (uint16_t)n : 0u; +} + + +void jlpSerialSetDtr(bool on) { + if (gCom != NO_PORT) { + (void)rs232SetDtr(gCom, on); + } +} + + +void jlpSerialSetRts(bool on) { + if (gCom != NO_PORT) { + (void)rs232SetRts(gCom, on); + } +} + + +// rs232Write waits on the UART itself and gives up while flow control +// holds the line, returning what went: the bounded poll JoeyLib asks for. +uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { + int32_t n; + + if (gCom == NO_PORT) { + return 0u; + } + n = rs232Write(gCom, (const char *)buf, len); + return n > 0 ? (uint16_t)n : 0u; +} diff --git a/src/x86/scanSet1.h b/src/x86/scanSet1.h new file mode 100644 index 0000000..e588881 --- /dev/null +++ b/src/x86/scanSet1.h @@ -0,0 +1,82 @@ +// PC keyboard scan code set 1 -> JoeyLib key, shared by the ports that see +// raw PC scan codes: DOS (its own INT 9 handler) and DVX (the held-key table +// DVX keeps from its INT 9 chain). Indexed by the make code with the break +// bit and any 0xE0 prefix stripped, so the grey arrows land on the same +// keys as the keypad ones, right Ctrl on KEY_LCTRL and right Alt on +// KEY_LALT. Slots left 0 (KEY_NONE) are keys JoeyLib does not name. +// +// A static table in a header: each includer gets its own read-only copy +// (DOS locks its copy for the interrupt handler that reads it). +#ifndef JOEY_SCAN_SET1_H +#define JOEY_SCAN_SET1_H + +#include + +#include "joey/input.h" + +// Make codes are 7 bits: the top bit of a scan byte is the break flag. +#define SCAN_SET1_CODES 128 + +static const uint8_t gScanSet1ToKey[SCAN_SET1_CODES] = { + [0x01] = KEY_ESCAPE, + [0x02] = KEY_1, + [0x03] = KEY_2, + [0x04] = KEY_3, + [0x05] = KEY_4, + [0x06] = KEY_5, + [0x07] = KEY_6, + [0x08] = KEY_7, + [0x09] = KEY_8, + [0x0A] = KEY_9, + [0x0B] = KEY_0, + [0x0E] = KEY_BACKSPACE, + [0x0F] = KEY_TAB, + [0x10] = KEY_Q, + [0x11] = KEY_W, + [0x12] = KEY_E, + [0x13] = KEY_R, + [0x14] = KEY_T, + [0x15] = KEY_Y, + [0x16] = KEY_U, + [0x17] = KEY_I, + [0x18] = KEY_O, + [0x19] = KEY_P, + [0x1C] = KEY_RETURN, + [0x1D] = KEY_LCTRL, + [0x1E] = KEY_A, + [0x1F] = KEY_S, + [0x20] = KEY_D, + [0x21] = KEY_F, + [0x22] = KEY_G, + [0x23] = KEY_H, + [0x24] = KEY_J, + [0x25] = KEY_K, + [0x26] = KEY_L, + [0x2A] = KEY_LSHIFT, + [0x2C] = KEY_Z, + [0x2D] = KEY_X, + [0x2E] = KEY_C, + [0x2F] = KEY_V, + [0x30] = KEY_B, + [0x31] = KEY_N, + [0x32] = KEY_M, + [0x36] = KEY_RSHIFT, + [0x38] = KEY_LALT, + [0x39] = KEY_SPACE, + [0x3B] = KEY_F1, + [0x3C] = KEY_F2, + [0x3D] = KEY_F3, + [0x3E] = KEY_F4, + [0x3F] = KEY_F5, + [0x40] = KEY_F6, + [0x41] = KEY_F7, + [0x42] = KEY_F8, + [0x43] = KEY_F9, + [0x44] = KEY_F10, + [0x48] = KEY_UP, + [0x4B] = KEY_LEFT, + [0x4D] = KEY_RIGHT, + [0x50] = KEY_DOWN, +}; + +#endif diff --git a/src/dos/spriteEmitX86.c b/src/x86/spriteEmitX86.c similarity index 99% rename from src/dos/spriteEmitX86.c rename to src/x86/spriteEmitX86.c index a6e627e..c004b64 100644 --- a/src/dos/spriteEmitX86.c +++ b/src/x86/spriteEmitX86.c @@ -1,4 +1,4 @@ -// x86 sprite codegen (DOS port). Emits 32-bit cdecl-callable PIC +// x86 sprite codegen (the DOS and DVX ports). Emits 32-bit cdecl-callable PIC // draw / save / restore routines that read or write 4bpp packed // surface bytes via [esi+disp8] chains. The C side calls them // through a function pointer cast. diff --git a/src/dos/dosTile.h b/src/x86/x86Tile.h similarity index 75% rename from src/dos/dosTile.h rename to src/x86/x86Tile.h index f50d8d4..4fe2bb6 100644 --- a/src/dos/dosTile.h +++ b/src/x86/x86Tile.h @@ -1,16 +1,16 @@ -// DOS whole-map tile paste engine (NATIVE-PERF Phase 6 W3). DOS was -// the only port whose jlTileMapPaste ran the generic per-cell path: -// each cell paid a cdecl call into jlpGenericTilePaste, a pixels -// check, a fresh (by+ty)*160 derive, and a ROLLED 8-iteration row -// loop. This engine hoists the check and the derive out of the loop +// x86 whole-map tile paste engine (NATIVE-PERF Phase 6 W3), shared by +// the DOS and DVX ports. DOS was the only port whose jlTileMapPaste ran +// the generic per-cell path: each cell paid a cdecl call into +// jlpGenericTilePaste, a pixels check, a fresh (by+ty)*160 derive, and +// a ROLLED 8-iteration row loop. This engine hoists the check and the derive out of the loop // and pastes each cell as 8 dword row moves at constant // displacements -- an 8x8 4bpp tile row is 4 bytes, exactly one // 32-bit load/store on the 386. Dispatched through the // jlpTileMapPaste alias from tile.c's shared whole-map arm (the W3 // seam; not a JL_HAS jlp op); tile.c owns the single union dirty // mark. Included from the tail of src/core/port.h like stTile.h. -#ifndef JOEY_DOS_TILE_H -#define JOEY_DOS_TILE_H +#ifndef JOEY_X86_TILE_H +#define JOEY_X86_TILE_H #include #include @@ -26,9 +26,9 @@ // caller's jlTileT source is odd-aligned, never a fault), so no // 68k-style parity guard or C fallback is needed; the destination is // always dword-aligned (row stride 160, column offset bx*4). -#define DOS_MAP_CELL_ROW(_r) memcpy(d + (_r) * SURFACE_BYTES_PER_ROW, s + (_r) * TILE_BYTES_PER_ROW, TILE_BYTES_PER_ROW) +#define X86_MAP_CELL_ROW(_r) memcpy(d + (_r) * SURFACE_BYTES_PER_ROW, s + (_r) * TILE_BYTES_PER_ROW, TILE_BYTES_PER_ROW) -static inline __attribute__((always_inline)) void dosTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) { +static inline __attribute__((always_inline)) void x86TileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) { const uint8_t *tileBytes; const uint8_t *s; const uint8_t *m; @@ -60,20 +60,20 @@ static inline __attribute__((always_inline)) void dosTileMapPaste(jlSurfaceT *ds // sizeof(jlTileT) is the tileset stride (32; pinned by // tile.c's tileMapStrideProof) -- gcc folds this to shl 5. s = tileBytes + (size_t)idx * sizeof(jlTileT); - DOS_MAP_CELL_ROW(0); - DOS_MAP_CELL_ROW(1); - DOS_MAP_CELL_ROW(2); - DOS_MAP_CELL_ROW(3); - DOS_MAP_CELL_ROW(4); - DOS_MAP_CELL_ROW(5); - DOS_MAP_CELL_ROW(6); - DOS_MAP_CELL_ROW(7); + X86_MAP_CELL_ROW(0); + X86_MAP_CELL_ROW(1); + X86_MAP_CELL_ROW(2); + X86_MAP_CELL_ROW(3); + X86_MAP_CELL_ROW(4); + X86_MAP_CELL_ROW(5); + X86_MAP_CELL_ROW(6); + X86_MAP_CELL_ROW(7); } d += TILE_BYTES_PER_ROW; } d += dRowStep; } } -#undef DOS_MAP_CELL_ROW +#undef X86_MAP_CELL_ROW #endif