Tools ported to C and bash. Some timing issues fixed on targets that support both NTSC and PAL regions.
This commit is contained in:
parent
154b116208
commit
c2be2e4b07
254 changed files with 29496 additions and 9130 deletions
59
LICENSES.md
59
LICENSES.md
|
|
@ -6,7 +6,7 @@ what is linked into or shipped with the programs it builds, what builds
|
|||
them, and what tests them. JoeyLib's own tree holds no third-party code or
|
||||
assets, apart from the patches in `installer/patches/`, which are under
|
||||
the licenses of the projects they patch (noted in those entries), and the
|
||||
BeOS palette in `tools/iconBake.py` (Haiku's, MIT; noted below).
|
||||
BeOS palette in `tools/iconBake/bePalette.h` (Haiku's, MIT; noted below).
|
||||
Components are grouped by role; each entry lists the platforms that use
|
||||
it, where it comes from, its license, and where the installer puts it.
|
||||
|
||||
|
|
@ -46,8 +46,9 @@ most.
|
|||
not in the archive.
|
||||
- **Role:** Apple IIgs Ensoniq DOC `.NTP` module replayer + SFX
|
||||
streamer. Merlin32-assembled into `ntpplayer`, which ships on the disk
|
||||
next to each IIgs program and is loaded at run time. Its
|
||||
`ntpconverter.php` makes the `.NTP` files (tools/joeymod).
|
||||
next to each IIgs program and is loaded at run time. The `.NTP` files it
|
||||
plays are made by JoeyLib's own `tools/modToNtp` (written from the
|
||||
archive's `file_format.txt`); the archive's PHP converter is not used.
|
||||
- **Installed at:** `toolchains/opt/ntp/` (source; installer component `ntp`)
|
||||
|
||||
### PTPlayer 6.4
|
||||
|
|
@ -405,15 +406,27 @@ above.
|
|||
- **Installed at:** `toolchains/opt/haikuTools/` (installer component
|
||||
`haikuTools`)
|
||||
|
||||
### The BeOS palette (in `tools/iconBake.py`)
|
||||
### The BeOS palette (in `tools/iconBake/bePalette.h`)
|
||||
|
||||
- **Used on:** Haiku, Haiku x86, BeOS
|
||||
- **URL:** <https://github.com/haiku/haiku> (`headers/private/interface/Palette.h`)
|
||||
- **License:** MIT (Haiku).
|
||||
- **Role:** the system's 256 colours, copied into `tools/iconBake.py` as
|
||||
`BE_PALETTE`, which the Be API programs' icons are baked to (an icon is
|
||||
- **Role:** the system's 256 colours, copied into `tools/iconBake/bePalette.h`
|
||||
as `kBePalette`, which the Be API programs' icons are baked to (an icon is
|
||||
indices into it; the palette itself is not in the program).
|
||||
- **Installed at:** nowhere; it is part of `tools/iconBake.py`.
|
||||
- **Installed at:** nowhere; it is part of `tools/iconBake`.
|
||||
|
||||
### PHP's sort order (in `tools/modToNtp/modToNtp.c`)
|
||||
|
||||
- **Used on:** Apple IIgs (the `.NTP` files `modToNtp` makes)
|
||||
- **URL:** <https://github.com/php/php-src> (`Zend/zend_sort.c`, PHP 8.3)
|
||||
- **License:** Zend Engine License 2.00; its quicksort is after LLVM
|
||||
libc++'s `std::sort` (MIT or the University of Illinois/NCSA license).
|
||||
- **Role:** `modToNtp` places a song's instruments and stream buffers in
|
||||
DOC RAM in the order Ninjaforce's converter, a PHP program, gets from
|
||||
PHP's sort, so its files match that converter's byte for byte. Its
|
||||
`sortClaims` (JoeyLib's code) takes the same steps `zend_sort` does.
|
||||
- **Installed at:** nowhere; it is part of `tools/modToNtp`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -421,13 +434,13 @@ above.
|
|||
|
||||
### Furnace 0.6.8.3
|
||||
|
||||
- **Used on:** every port (chip-tracker music, through `tools/vgm2song.py`)
|
||||
- **Used on:** every port (chip-tracker music, through `tools/vgm2song`)
|
||||
- **URL:** <https://github.com/tildearrow/furnace>
|
||||
- **License:** GPLv2 or later. Its demo songs are not under the GPL: they are
|
||||
their authors', all rights reserved, and nothing made from them is kept or
|
||||
shipped (`scripts/check-vgm.sh` converts them in a scratch folder).
|
||||
- **Role:** the recommended tracker for writing chip-tracker songs
|
||||
(docs/Assets.adoc); its VGM export is what `tools/vgm2song.py` converts.
|
||||
(docs/Assets.adoc); its VGM export is what `tools/vgm2song` converts.
|
||||
- **Installed at:** `toolchains/opt/furnace/` (installer component `furnace`,
|
||||
optional: `./install.sh install furnace`)
|
||||
|
||||
|
|
@ -513,25 +526,27 @@ developer's machine. Not part of any redistributed artifact. Installed by
|
|||
## Distribution packages (build and test hosts only)
|
||||
|
||||
Installed from the Linux distribution by the components named below
|
||||
(`host`, `headless`, `ntp`); none of them ships in a program.
|
||||
(`host` and `headless`); none of them ships in a program.
|
||||
|
||||
- **host:** GNU make, the host C compiler (gcc), tar and diffutils (GNU GPL
|
||||
v3+); git and socat (GNU GPL v2); git-lfs (MIT); curl (the curl
|
||||
license); xz (0BSD, parts GPL); bzip2 (bzip2's BSD-style license);
|
||||
unzip (the Info-ZIP license); lhasa (ISC); procps (GNU GPL v2+ and LGPL);
|
||||
Python 3 (PSF License) with Pillow (MIT-CMU).
|
||||
- **host:** GNU make, the host C compiler (gcc), gawk, patch, tar, and
|
||||
diffutils (GNU GPL v3+); git and socat (GNU GPL v2); potrace (GNU GPL
|
||||
v2+); git-lfs (MIT); curl (the curl license); xz (0BSD, parts GPL);
|
||||
bzip2 (bzip2's BSD-style license); unzip (the Info-ZIP license); lhasa
|
||||
(ISC); procps (GNU GPL v2+ and LGPL).
|
||||
- **headless:** Xvfb (X.Org's MIT/X11 license); xdotool (3-clause BSD);
|
||||
ImageMagick (the ImageMagick License); pactl and parec, from
|
||||
pulseaudio-utils (GNU LGPL v2.1+).
|
||||
- **ntp:** PHP's command line (the PHP License 3.01) runs NTP's converter.
|
||||
- **tools/traceLogo.py**, which made `assets/joeylibLogo.svg` and is run
|
||||
only if the logo art changes, takes potracer (GNU GPL v2+), Pillow and
|
||||
numpy (BSD), installed by hand with pip. The script holds none of
|
||||
potracer's code, and the logo it made is not covered by potracer's
|
||||
license.
|
||||
- **tools/traceLogo**, which made `assets/joeylibLogo.svg` and is run
|
||||
only if the logo art changes, runs the potrace program (Peter Selinger's,
|
||||
GNU GPL v2+; the distribution's `potrace` package, which the `host`
|
||||
component installs) as a separate program. The tool holds none of
|
||||
potrace's code, and the logo it made is not covered by potrace's license.
|
||||
- Building MAME, QEMU, PPSSPP, and SDL 3 also takes the distribution's
|
||||
development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib, and the
|
||||
rest), each under its own license.
|
||||
rest), each under its own license; they, llvm816, the Haiku compilers and
|
||||
tools, emsdk, and elf2x68k (whose linker runs a Python script of its own)
|
||||
also take Python 3 (PSF License) to build or run. JoeyLib's own tools and
|
||||
gates hold no Python.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
3
Makefile
3
Makefile
|
|
@ -22,6 +22,7 @@
|
|||
# make haiku builds Haiku x86_64 (the Be API backend, gcc 13)
|
||||
# make haiku32 builds Haiku 32-bit x86 (the Be API backend, gcc 2.95, BeOS ABI)
|
||||
# make beos builds BeOS R5 x86 (the same backend and compiler, R5's own headers and libraries)
|
||||
# make tools builds the host tools (the asset, icon, sprite, and music converters) into build/tools
|
||||
# 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)
|
||||
|
|
@ -133,7 +134,7 @@ verify-audio:
|
|||
@$(REPO_DIR)/scripts/verify-all.sh audio $(PORTS)
|
||||
|
||||
# Every recording port against the one audio contract (AUDPAR, graded by
|
||||
# tools/gradeAudioConformance.py), sequentially, with the same exit codes.
|
||||
# tests/gate/audioGate.c), sequentially, with the same exit codes.
|
||||
# Takes a subset: make verify-conform PORTS="sdl3 dos"
|
||||
verify-conform:
|
||||
@$(REPO_DIR)/scripts/verify-all.sh conform $(PORTS)
|
||||
|
|
|
|||
72
README.md
72
README.md
|
|
@ -173,24 +173,25 @@ make -f make/x68000.mk EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE
|
|||
## Conformance gate
|
||||
|
||||
`examples/uber` is the conformance vehicle: it exercises every public op and
|
||||
logs a surface hash per op. `tools/diff-uber-hashes <reference-log> <test-log>`
|
||||
compares two `joeylog.txt` captures and exits non-zero on any mismatch. The
|
||||
logs a surface hash per op.
|
||||
`build/tools/diffUberHashes <reference-log> <test-log>` (`make tools` builds
|
||||
it) 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, and DVX, SDL3, the
|
||||
web, and the PSP, which share the DOS stage, are diffed against
|
||||
`tests/goldens/uber/dos.txt` (`scripts/bench-dvx.sh`, `scripts/bench-sdl3.sh`,
|
||||
`scripts/bench-web.sh`, or `scripts/bench-psp.sh`, then
|
||||
`tools/diff-uber-hashes`). SDL3's run needs no emulator and no X server
|
||||
`build/tools/diffUberHashes`). SDL3's run needs no emulator and no X server
|
||||
(SDL's dummy drivers) and takes about fifteen seconds; the web's runs UBER's
|
||||
page in headless Chromium; the PSP's runs UBER under headless PPSSPP in a few
|
||||
seconds, with every memory access checked as a PSP checks it, so an unaligned
|
||||
16- or 32-bit access anywhere fails it:
|
||||
|
||||
```
|
||||
scripts/bench-sdl3.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
|
||||
scripts/bench-web.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
|
||||
scripts/bench-psp.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/psp/bin/UBER/joeylog.txt
|
||||
scripts/bench-sdl3.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
|
||||
scripts/bench-web.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
|
||||
scripts/bench-psp.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/psp/bin/UBER/joeylog.txt
|
||||
```
|
||||
|
||||
The X68000 has gates of its own:
|
||||
|
|
@ -232,17 +233,32 @@ src/m68k/ 68k code shared across machines (planar and word-
|
|||
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)
|
||||
tools/spritebake/ offline .spr -> .spc pre-compiled sprite baker
|
||||
tools/songbake/ chip-tracker text notation -> JYM1 stream
|
||||
tools/vgm2song.py a tracker's VGM export (Furnace) -> chip-tracker .song
|
||||
tools/iconBake.py one picture -> every system's program icon
|
||||
tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer
|
||||
tools/diff-uber-hashes UBER golden-hash comparator
|
||||
tools/ the host tools, all C (make tools builds them into build/tools/):
|
||||
adfTool/ read files out of an Amiga disk image (.adf)
|
||||
assetbake/ PNG -> native .tbk / .spr baker
|
||||
common/ the C tools' shared code (PNG, DEFLATE, XML, JSON, base64)
|
||||
diffUberHashes/ UBER golden-hash comparator
|
||||
diffUberPerf/ UBER ops/sec comparator
|
||||
iconBake/ one picture -> every system's program icon
|
||||
iigsStart/ GS/OS boot launcher for the headless IIgs harnesses
|
||||
joeymod/ Protracker .MOD converter (passthrough, .amod, or .NTP)
|
||||
modloopend/ an E8F stop marker added to a .MOD (the Amiga's .amod)
|
||||
modToNtp/ Protracker .MOD -> NinjaTrackerPlus .NTP (the IIgs)
|
||||
rawCd/ a raw CD image's track or El Torito boot floppy
|
||||
scanPtrBug/ scan IIgs assembly for a far-pointer miscompile pattern
|
||||
songbake/ chip-tracker text notation -> JYM1 stream
|
||||
spritebake/ offline .spr -> .spc pre-compiled sprite baker
|
||||
szddExpand/ expand a COMPRESS.EXE (SZDD) file
|
||||
tiled2joey/ a Tiled map (TMX or TMJ) -> tile sheet + tile maps
|
||||
traceLogo/ the logo PNG -> its traced SVG (runs potrace)
|
||||
uberPerfTable/ the UBER ops/sec table across ports
|
||||
vgm2song/ a tracker's VGM export (Furnace) -> chip-tracker .song
|
||||
winIni/ edit a Windows 3.x .INI file
|
||||
xdftool/ Human68k FAT12 .XDF disk image reader / writer
|
||||
examples/ example programs
|
||||
joey-run run a built program in its emulator
|
||||
scripts/ bench / verification scripts (scripts/lib: shared helpers)
|
||||
tests/gate/ the gates' C helpers (screenshot, audio, and serial checks)
|
||||
tests/goldens/uber/ frozen UBER conformance hashes
|
||||
install.sh installs toolchains, emulators and firmware
|
||||
installer/ the installer's component recipes, patches, lists
|
||||
|
|
@ -410,7 +426,7 @@ void jlTilePasteMono (jlSurfaceT *dst, uint8_t bx, uint8_t by,
|
|||
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
|
||||
|
||||
// Load up to maxTiles tiles from a baked .tbk file (per-target planar
|
||||
// bytes from tools/assetbake/assetbake.py --type tile --target ...).
|
||||
// bytes from tools/assetbake --type tile --target ...).
|
||||
// Refuses files baked for the wrong target. outValid (optional) marks
|
||||
// each loaded slot; outPalette (optional) receives the embedded
|
||||
// 16-entry $0RGB palette if present.
|
||||
|
|
@ -448,7 +464,7 @@ jlSpriteT *jlSpriteCreateFromSurface (const jlSurfaceT *src, int16_t x, int16_t
|
|||
void jlSpriteDestroy (jlSpriteT *sp);
|
||||
|
||||
// Load up to maxCels cels from a baked .spr file (cross-target chunky
|
||||
// 4bpp blob from tools/assetbake/assetbake.py --type sprite). Each
|
||||
// 4bpp blob from tools/assetbake --type sprite). Each
|
||||
// cel becomes a freshly-allocated jlSpriteT (release with
|
||||
// jlSpriteDestroy). outPalette (optional) receives the embedded
|
||||
// 16-entry $0RGB palette if present.
|
||||
|
|
@ -494,8 +510,8 @@ uint32_t jlSpriteCodegenBytesTotal (void);
|
|||
### Assets (baked at build time)
|
||||
|
||||
Asset PNGs live in each example's `assets/` directory and are baked
|
||||
to native binary blobs at build time by `tools/assetbake/assetbake.py`
|
||||
(Python+PIL). The runtime loads those blobs directly with no
|
||||
to native binary blobs at build time by `tools/assetbake` (C, built
|
||||
into `build/tools/`). The runtime loads those blobs directly with no
|
||||
conversion -- there is no in-memory chunky-to-planar or palette
|
||||
re-indexing on the device.
|
||||
|
||||
|
|
@ -513,8 +529,8 @@ Two blob formats:
|
|||
Both formats embed an optional 16-entry `$0RGB` palette.
|
||||
|
||||
```
|
||||
tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs|x68000} in.png out.tbk
|
||||
tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
|
||||
build/tools/assetbake --type tile --target {amiga|atarist|dos|iigs|x68000} in.png out.tbk
|
||||
build/tools/assetbake --type sprite --cell WxH in.png out.spr
|
||||
```
|
||||
|
||||
A program's description wires its bake rules once for every target
|
||||
|
|
@ -533,7 +549,7 @@ on-the-fly path stays alongside the baked load API.
|
|||
Every program gets an icon on each system that shows programs by one --
|
||||
the IIgs Finder, Workbench, the Program Managers of DVX and Windows 3.1,
|
||||
Explorer, Tracker, the PSP's menu, the SDL3 window, and the web page --
|
||||
baked by `tools/iconBake.py` from one picture: the program's `ID_ICON` (a
|
||||
baked by `tools/iconBake` from one picture: the program's `ID_ICON` (a
|
||||
PNG), or JoeyLib's kangaroo (`assets/joeyIcon.png`). `docs/BuildIntegration.adoc`
|
||||
(Icons) says what each system gets, and `scripts/verify-all.sh icon` checks
|
||||
that each one shows it.
|
||||
|
|
@ -543,7 +559,7 @@ that each one shows it.
|
|||
`docs/Assets.adoc` (Content Creation Tools) recommends a tool for each kind
|
||||
of asset: GrafX2 or LibreSprite for tile and sprite sheets, Furnace for
|
||||
chip-tracker music (`./install.sh install furnace`; its VGM exports become
|
||||
`.song` files through `tools/vgm2song.py`), MilkyTracker or OpenMPT for
|
||||
`.song` files through `tools/vgm2song`), MilkyTracker or OpenMPT for
|
||||
`.MOD` music, and sfxr, jfxr, Audacity, or SoX for sound effects.
|
||||
|
||||
|
||||
|
|
@ -643,7 +659,7 @@ void jlAudioStopSfx (uint8_t slot);
|
|||
|
||||
// Portable chip-tracker music: a JYM1 event stream baked by tools/songbake
|
||||
// (from text notation, or from a tracker: write it in Furnace for the
|
||||
// SN76489 or AY-3-8910, export VGM, convert with tools/vgm2song.py),
|
||||
// SN76489 or AY-3-8910, export VGM, convert with tools/vgm2song),
|
||||
// played through the tone/noise layer at register-write cost on every port.
|
||||
// No CPU mixing, so it is viable where jlAudioPlayMod is not.
|
||||
bool jlMusicPlay (const uint8_t *data, uint32_t length, bool loop);
|
||||
|
|
@ -769,9 +785,13 @@ Building with `EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE` additionally mirrors every
|
|||
unbounded, needs no disk, and arrives in real time, which is what makes a
|
||||
wedged run diagnosable. It is OFF by default and is currently implemented
|
||||
only for the X68000 (`jlpLogTee` in `src/x68000/hal.c`); the host end is
|
||||
`scripts/x68kSerialPeer.py`, which must be run under `python3 -u` -- it never
|
||||
passes `flush=True`, so a killed peer loses its whole capture to stdio
|
||||
buffering.
|
||||
the `x68kSerialPeer` gate tool (`tests/gate/x68kSerialPeer.c`, built by
|
||||
`make -f make/tools.mk x68kSerialPeer` into `build/tools/`), whose output is
|
||||
line-buffered, so a killed peer keeps everything it captured:
|
||||
|
||||
```
|
||||
build/tools/x68kSerialPeer --listen-only --until '----- end -----' --timeout 3600 | tee x68.log
|
||||
```
|
||||
|
||||
|
||||
### Platform macros (`joey/platform.h`)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 34 KiB |
|
|
@ -233,8 +233,8 @@ uint16_t jlFrameHz(void);
|
|||
*Parameters:* none.
|
||||
|
||||
*Returns:* the nominal display frame rate in Hz -- 60 on the IIgs, or 50 on a
|
||||
PAL machine; 50 on the Amiga, whose screen is PAL, or 60 on an NTSC Amiga that
|
||||
cannot show PAL; 50 or 60 on the ST, as the machine is set (71 with a
|
||||
PAL machine; 50 on a PAL Amiga and 60 on an NTSC one, whose screen is in the
|
||||
machine's own standard; 50 or 60 on the ST, as the machine is set (71 with a
|
||||
monochrome monitor); 55 on the X68000; 70 on DOS's VGA and on DVX, whose
|
||||
window has a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE
|
||||
mode's, with the same frame count); and 60 on every hosted port, whose window
|
||||
|
|
|
|||
|
|
@ -12,17 +12,17 @@ type: what you author, which tool bakes it, and which API call loads it.
|
|||
|You author |Tool |Baked file |Loaded by
|
||||
|
||||
|Indexed PNG tile sheet
|
||||
|`assetbake.py --type tile`
|
||||
|`assetbake --type tile`
|
||||
|`.tbk`
|
||||
|`jlTileBankLoad`, `jlTileBankLoadToSurface`
|
||||
|
||||
|A Tiled map (TMX or TMJ) with indexed tilesets
|
||||
|`tiled2joey.py`, then `assetbake.py --type tile` for its sheet
|
||||
|`tiled2joey`, then `assetbake --type tile` for its sheet
|
||||
|a `.map` per layer, and a `.tbk`
|
||||
|`jlTileMapPaste` (the map read with `jlDataOpen`)
|
||||
|
||||
|Indexed PNG sprite sheet
|
||||
|`assetbake.py --type sprite`
|
||||
|`assetbake --type sprite`
|
||||
|`.spr`
|
||||
|`jlSpriteBankLoad`
|
||||
|
||||
|
|
@ -32,12 +32,12 @@ type: what you author, which tool bakes it, and which API call loads it.
|
|||
|`jlSpriteBankLoadPrecompiled`
|
||||
|
||||
|Text note notation
|
||||
|`songbake.py`
|
||||
|`songbake`
|
||||
|`.jym` or a C header
|
||||
|`jlMusicPlay`
|
||||
|
||||
|A tracker song (Furnace) for the SN76489 or AY-3-8910, exported as VGM
|
||||
|`vgm2song.py`, then `songbake.py`
|
||||
|`vgm2song`, then `songbake`
|
||||
|`.jym` or a C header
|
||||
|`jlMusicPlay`
|
||||
|
||||
|
|
@ -55,6 +55,9 @@ type: what you author, which tool bakes it, and which API call loads it.
|
|||
Baked assets are staged into `DATA/` beside the executable, and the program
|
||||
opens them with bare names.
|
||||
|
||||
The bakers are C programs built for the computer you build on: `make tools`
|
||||
puts them in `build/tools/`, and a port's build makes the ones its rules run.
|
||||
|
||||
=== Content Creation Tools
|
||||
|
||||
These are the tools we recommend for making each kind of asset. All of them
|
||||
|
|
@ -77,7 +80,7 @@ run on Linux, and all but OpenMPT are native there.
|
|||
|Tile maps
|
||||
|*Tiled* (GPLv2; what you make with it is yours), a map editor that places
|
||||
tiles from tilesets in layers.
|
||||
|Orthogonal maps of 8x8 tiles from indexed tilesets, then `tiled2joey.py`
|
||||
|Orthogonal maps of 8x8 tiles from indexed tilesets, then `tiled2joey`
|
||||
(<<_maps_from_tiled>>).
|
||||
|
||||
|Program icons
|
||||
|
|
@ -131,7 +134,7 @@ row by row.
|
|||
|
||||
[source,console]
|
||||
----
|
||||
$ python3 tools/assetbake/assetbake.py --type tile --target atarist \
|
||||
$ build/tools/assetbake --type tile --target atarist \
|
||||
font.png build/atarist/bin/DATA/font.tbk
|
||||
----
|
||||
|
||||
|
|
@ -145,16 +148,16 @@ loader refuses a file baked for a different target rather than drawing garbage.
|
|||
|
||||
==== Maps from Tiled
|
||||
|
||||
`tools/tiled2joey.py` turns a map made in Tiled into what a JoeyLib program
|
||||
pastes: a tile sheet, which `assetbake.py` bakes into a tile bank for each
|
||||
`tools/tiled2joey` turns a map made in Tiled into what a JoeyLib program
|
||||
pastes: a tile sheet, which `assetbake` bakes into a tile bank for each
|
||||
port, and a map for each tile layer, which `jlTileMapPaste` takes as it is:
|
||||
|
||||
[source,console]
|
||||
----
|
||||
$ python3 tools/tiled2joey.py --header build/level1.h level1.tmx \
|
||||
$ build/tools/tiled2joey --header build/level1.h level1.tmx \
|
||||
build/level1.png build/level1
|
||||
tiled2joey: 60x25 map, 87 tiles, 14 colours, layers: ground.map, detail.map
|
||||
$ python3 tools/assetbake/assetbake.py --type tile --target iigs \
|
||||
$ build/tools/assetbake --type tile --target iigs \
|
||||
build/level1.png build/iigs/bin/DATA/level1.tbk
|
||||
----
|
||||
|
||||
|
|
@ -202,7 +205,7 @@ layer = jlTileLayerCreate(tiles, level, LEVEL1_WIDTH, LEVEL1_HEIGHT, 0, 25);
|
|||
|
||||
[source,console]
|
||||
----
|
||||
$ python3 tools/assetbake/assetbake.py --type sprite --cell 2x3 \
|
||||
$ build/tools/assetbake --type sprite --cell 2x3 \
|
||||
player.png build/atarist/bin/DATA/player.spr
|
||||
----
|
||||
|
||||
|
|
@ -295,8 +298,8 @@ Bake it either as a binary blob or directly as a C array:
|
|||
|
||||
[source,console]
|
||||
----
|
||||
$ python3 tools/songbake/songbake.py song.song song.jym
|
||||
$ python3 tools/songbake/songbake.py -c gDemoSong song.song song.h
|
||||
$ build/tools/songbake song.song song.jym
|
||||
$ build/tools/songbake -c gDemoSong song.song song.h
|
||||
----
|
||||
|
||||
The C header form is usually what you want for a small song: it compiles into
|
||||
|
|
@ -313,7 +316,7 @@ same time; `songbake` warns when they overlap.
|
|||
|
||||
A tracker is easier than a text file for anything longer than a jingle. Write
|
||||
the song in Furnace, export it as VGM (a log of the chip's register writes as
|
||||
the song plays), and `tools/vgm2song.py` turns that into a `.song` file, which
|
||||
the song plays), and `tools/vgm2song` turns that into a `.song` file, which
|
||||
is still plain text you can read and touch up:
|
||||
|
||||
. Start the song on a system with a *TI SN76489* (the Sega Master System, BBC
|
||||
|
|
@ -328,8 +331,8 @@ is still plain text you can read and touch up:
|
|||
+
|
||||
[source,console]
|
||||
----
|
||||
$ python3 tools/vgm2song.py song.vgm song.song
|
||||
$ python3 tools/songbake/songbake.py -c gSong song.song song.h
|
||||
$ build/tools/vgm2song song.vgm song.song
|
||||
$ build/tools/songbake -c gSong song.song song.h
|
||||
----
|
||||
|
||||
What comes across:
|
||||
|
|
@ -384,10 +387,9 @@ The output extension selects the conversion:
|
|||
does not loop at the marker too, and the IIgs's replayer ignores `E8F`, so
|
||||
an `.amod` plays the same anywhere (looping, with the marker's pattern of
|
||||
silence at its end).
|
||||
* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the
|
||||
NinjaTrackerPlus converter the installer's `ntp` component provides (part of
|
||||
`toolchain:iigs`; it needs the PHP command-line interpreter, which the
|
||||
installer's host packages include).
|
||||
* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, made by JoeyLib's
|
||||
`modToNtp` (`build/tools/modToNtp IN.MOD OUT.NTP`, which `joeymod` builds
|
||||
and runs). Its files are byte for byte those of Ninjaforce's own converter.
|
||||
|
||||
Keep modules modest. A four-channel mix is real CPU work on DOS, the ST, and
|
||||
the X68000, and the sample data has to fit in memory alongside your game.
|
||||
|
|
@ -459,18 +461,19 @@ And a bake rule looks like:
|
|||
|
||||
[source,makefile]
|
||||
----
|
||||
$(call programDataDir,MYGAME)/font.tbk: assets/font.png
|
||||
$(call programDataDir,MYGAME)/font.tbk: assets/font.png $(ASSET_BAKE)
|
||||
@mkdir -p $(dir $@)
|
||||
python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@
|
||||
$(ASSET_BAKE) --type tile --target $(ASSET_PLATFORM) $< $@
|
||||
----
|
||||
|
||||
Tile banks and `.spc` files differ per platform, while `.spr`, `.jym`, and
|
||||
`.sfx` files are shared, as are the `.map` files `tiled2joey.py` writes:
|
||||
`.sfx` files are shared, as are the `.map` files `tiled2joey` writes:
|
||||
`ASSET_PLATFORM` names the target a bake is for (DVX and every hosted port --
|
||||
SDL3, the web, macOS, Win32, Win16, the PSP, Haiku, and BeOS -- take the DOS
|
||||
bakes; DVX and Win16 run the x86 code of a `.spc`, the others use only its
|
||||
cels; the STe build takes the ST's) and `SPRITEBAKE` the matching host
|
||||
`spritebake`.
|
||||
cels; the STe build takes the ST's), `SPRITEBAKE` the matching host
|
||||
`spritebake`, and `ASSET_BAKE` the host `assetbake`; a rule that runs a baker
|
||||
lists it as a prerequisite, so make builds it first.
|
||||
|
||||
`examples/tilebank` is a working reference: one rule in `make/examples.mk`
|
||||
bakes the same PNG for each target into its `DATA/tilebank` (`TILEBANK_DATA`),
|
||||
|
|
@ -509,16 +512,21 @@ and the streaming loader needs only a small internal scratch buffer.
|
|||
=== The Logo
|
||||
|
||||
The JoeyLib logo lives in `assets/` as both the original 320x200 PNG and a
|
||||
traced SVG. If the source art ever changes, regenerate the vector with:
|
||||
traced SVG. If the source art ever changes, regenerate the vector with
|
||||
`tools/traceLogo`, which needs the potrace program (the distribution's
|
||||
`potrace` package, or set `POTRACE` to one installed elsewhere):
|
||||
|
||||
[source,console]
|
||||
----
|
||||
$ python3 -m venv /tmp/tracevenv
|
||||
$ /tmp/tracevenv/bin/pip install potracer pillow numpy
|
||||
$ /tmp/tracevenv/bin/python tools/traceLogo.py \
|
||||
assets/joeylib_logo_-_320x200.png assets/joeylibLogo.svg --colors 5
|
||||
$ make -f make/tools.mk traceLogo
|
||||
$ build/tools/traceLogo assets/joeylib_logo_-_320x200.png \
|
||||
assets/joeylibLogo.svg
|
||||
----
|
||||
|
||||
Five colour clusters measured as the closest match to the original artwork;
|
||||
more clusters start carving the antialiased edges into separate layers, which
|
||||
renders as noise rather than detail.
|
||||
The art is four flat colours (ink, skin, the green shorts, and the beige
|
||||
monitor), so the default of four clusters is the trace that looks best: each
|
||||
layer is filled with the exact colour most of its pixels have, and the
|
||||
clustering is done in CIELAB, where an antialiased grey edge joins the ink or
|
||||
the beige it lies between rather than the skin colour nearest it in RGB. A
|
||||
fifth cluster and beyond only carve those grey edges into layers of their
|
||||
own, which render as speckled outlines rather than detail.
|
||||
|
|
|
|||
|
|
@ -204,7 +204,8 @@ the others take its cels without their code. The STe build takes the ST's.
|
|||
|`SPRITEBAKE` |The host `spritebake` for this target (its rule is
|
||||
JoeyLib's).
|
||||
|`PROJECT_DIR`, `PROJECT_BUILD` |The project and its `build/`.
|
||||
|`REPO_DIR` |The JoeyLib checkout, for its tools: `$(REPO_DIR)/tools/assetbake/assetbake.py`.
|
||||
|`ASSET_BAKE` |The host `assetbake` (its rule is JoeyLib's).
|
||||
|`REPO_DIR` |The JoeyLib checkout.
|
||||
|`HOST_CC`, `HOST_CFLAGS` |For a host tool of the project's own.
|
||||
|===
|
||||
|
||||
|
|
@ -220,13 +221,13 @@ $(MYGAME_RUN)/theme.jym: assets/theme.jym
|
|||
@mkdir -p $(dir $@)
|
||||
cp $< $@
|
||||
|
||||
$(MYGAME_RUN)/tiles.tbk: assets/tiles.png
|
||||
$(MYGAME_RUN)/tiles.tbk: assets/tiles.png $(ASSET_BAKE)
|
||||
@mkdir -p $(dir $@)
|
||||
python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@
|
||||
$(ASSET_BAKE) --type tile --target $(ASSET_PLATFORM) $< $@
|
||||
|
||||
$(MYGAME_GEN)/player.spr: assets/player.png
|
||||
$(MYGAME_GEN)/player.spr: assets/player.png $(ASSET_BAKE)
|
||||
@mkdir -p $(dir $@)
|
||||
python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type sprite --cell 16x16 $< $@
|
||||
$(ASSET_BAKE) --type sprite --cell 16x16 $< $@
|
||||
|
||||
$(MYGAME_RUN)/player.spc: $(MYGAME_GEN)/player.spr $(SPRITEBAKE)
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -333,7 +334,7 @@ stick; the PSP shows it by its icon and its `ID_TITLE`. The unstripped ELF stays
|
|||
|
||||
A program has an icon on every system that shows programs by one. `ID_ICON`
|
||||
names a picture, a PNG, best square and transparent around the figure, and
|
||||
`make` bakes it for each target with `tools/iconBake.py`; a program without
|
||||
`make` bakes it for each target with `tools/iconBake`; a program without
|
||||
one gets JoeyLib's kangaroo, `assets/joeyIcon.png`. A picture of 64 pixels or
|
||||
less is taken for pixel art and enlarged by whole pixels; a bigger one is
|
||||
resampled smoothly. Where an icon has few colours, each pixel takes the
|
||||
|
|
@ -469,7 +470,7 @@ if a gate had to be skipped.
|
|||
|`scripts/bench-<port>.sh`
|
||||
|Per-operation throughput for that port.
|
||||
|
||||
|`tools/diff-uber-hashes`
|
||||
|`build/tools/diffUberHashes`
|
||||
|Compares a port's rendered output against the IIgs golden reference,
|
||||
operation by operation, by surface hash.
|
||||
|===
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ The API is uniform; these are the places where what you get differs.
|
|||
|
||||
|Frame rate
|
||||
|60 Hz (50 on a PAL machine)
|
||||
|50 Hz (PAL; 60 on an NTSC machine without PAL)
|
||||
|50 Hz on a PAL machine, 60 on an NTSC one
|
||||
|50 or 60 Hz, as the machine is set
|
||||
|70 Hz
|
||||
|55 Hz (56 under MAME)
|
||||
|
|
|
|||
|
|
@ -140,8 +140,8 @@ too.
|
|||
|
||||
==== Frame-Rate Independence
|
||||
|
||||
The targets do not share a refresh rate: 50 Hz (the Amiga, and a PAL IIgs or
|
||||
ST), 55 Hz (X68000), 60 Hz (the IIgs, an NTSC ST, and every hosted port's
|
||||
The targets do not share a refresh rate: 50 Hz (a PAL Amiga, IIgs, or ST),
|
||||
55 Hz (X68000), 60 Hz (an NTSC Amiga, IIgs, or ST, and every hosted port's
|
||||
virtual rate), 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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Generated by tools/songbake/songbake.py from examples/audio/demoSong.song.
|
||||
// Generated by tools/songbake from examples/audio/demoSong.song.
|
||||
// Do not hand-edit; edit the .song source and rebake:
|
||||
// python3 tools/songbake/songbake.py -c gDemoSong examples/audio/demoSong.song <this file>
|
||||
// build/tools/songbake -c gDemoSong examples/audio/demoSong.song <this file>
|
||||
|
||||
#ifndef JOEYLIB_BAKED_GDEMOSONG_H
|
||||
#define JOEYLIB_BAKED_GDEMOSONG_H
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
; around the drum rows because the ST shares channel C's volume
|
||||
; between tone voice 2 and noise.
|
||||
;
|
||||
; Bake: python3 tools/songbake/songbake.py -c gDemoSong \
|
||||
; Bake: build/tools/songbake -c gDemoSong \
|
||||
; examples/audio/demoSong.song examples/audio/demoSong.h
|
||||
|
||||
tickms 110
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
// select-then-write register sequence in the path of both channels'
|
||||
// interrupts for the whole run.
|
||||
// SPACE pauses and resumes transmitting. The peer answers byte for byte
|
||||
// (scripts/lib/serialStreamPeer.py), so a pause quiets the line both ways.
|
||||
// (tests/gate/serialStreamPeer.c), so a pause quiets the line both ways.
|
||||
// ESC quits.
|
||||
//
|
||||
// Mouse: a small pointer follows it.
|
||||
|
|
@ -290,7 +290,7 @@ static void stampSerialRow(jlSurfaceT *screen, const StreamStatsT *stats, uint16
|
|||
// Byte `index` of a stream: the index's low byte XOR its next byte, XOR the
|
||||
// stream's mask. A lost or repeated byte throws every byte after it off its
|
||||
// expected value, where a plain counter would miss any gap that is a multiple
|
||||
// of 256. scripts/lib/serialStreamPeer.py uses the same formula and masks.
|
||||
// of 256. tests/gate/serialStreamPeer.c uses the same formula and masks.
|
||||
static uint8_t streamByte(uint32_t index, uint8_t mask) {
|
||||
return (uint8_t)(index ^ (index >> BYTE_BITS) ^ mask);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
// that assetbake's encoder for THIS target writes exactly the layout this
|
||||
// port's native tile paste reads, and that this build refuses the others.
|
||||
//
|
||||
// DATA/tilebank/ is staged by tests/host/tileBankStage.py (the make rule in
|
||||
// make/<port>.mk), from tests/host/tileBank.png:
|
||||
// DATA/tilebank/ is staged by tests/host/tileBankStage.sh (the make rule in
|
||||
// make/examples.mk), from tests/host/tileBank.png:
|
||||
// own.tbk this port's bake: the bank that must load
|
||||
// refuse<n>.tbk n = 1, 2, ...: every other target's bake
|
||||
// expect.bin the PNG decoded by PIL, independent of assetbake's
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
// ----- Constants -----
|
||||
|
||||
// The staged files (tests/host/tileBankStage.py names them).
|
||||
// The staged files (tests/host/tileBankStage.sh names them).
|
||||
#define OWN_BANK "tilebank/own.tbk"
|
||||
#define EXPECT_NAME "tilebank/expect.bin"
|
||||
#define REFUSE_BANK_FORMAT "tilebank/refuse%u.tbk"
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
|
|||
// regardless of CPU speed or display refresh rate. Also logs an
|
||||
// FNV-1a hash of the surface state after timing -- this is the
|
||||
// pixel-perfect comparison input for the cross-port validation
|
||||
// harness (tools/diff-uber-hashes.py). Captured against IIgs as the
|
||||
// harness (build/tools/diffUberHashes). Captured against IIgs as the
|
||||
// golden reference; planar 68k rewrites validate by matching it.
|
||||
// The window is `frames` long. settle (NULL for none) runs between the
|
||||
// timing and the hash, for an op whose last call leaves a picture that
|
||||
|
|
@ -1704,7 +1704,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
|
|||
// ----- Non-timed correctness checks (Phase 0 verification harness) -----
|
||||
//
|
||||
// Each check draws a deterministic scene and logs the surface hash so
|
||||
// tools/diff-uber-hashes can compare ports against each other and against
|
||||
// build/tools/diffUberHashes can compare ports against each other and against
|
||||
// the frozen golden logs. Several checks intentionally capture the CURRENT
|
||||
// behavior of known bugs (claudeDocs/PERF-AUDIT.md #1, #11, #12, #72): their hash
|
||||
// lines are EXPECTED to change when the Phase 1 fixes land -- re-golden
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ static bool launch(const char *folder, const char *name, ULONG stack);
|
|||
static void logLine(const char *format, ...);
|
||||
|
||||
// Workbench's four colours (2.0 on), 4 bits a gun: grey, black, white and
|
||||
// blue (tools/iconBake.py's AMIGA_COLOURS).
|
||||
// blue (tools/iconBake/iconBake.c's kAmigaColours).
|
||||
static const UWORD gWorkbenchColours[] = { 0xAAA, 0x000, 0xFFF, 0x68B };
|
||||
|
||||
static FILE *gLog = NULL;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ typedef struct {
|
|||
} jlSpriteBackupT;
|
||||
|
||||
// Load up to maxCels cels from a baked .spr file produced by
|
||||
// tools/assetbake/assetbake.py. Each cel becomes a freshly-allocated
|
||||
// tools/assetbake/assetbake.c. Each cel becomes a freshly-allocated
|
||||
// jlSpriteT (release with jlSpriteDestroy); the file's cellCount is
|
||||
// capped at maxCels.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSur
|
|||
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
|
||||
|
||||
// Load up to maxTiles tiles from a baked .tbk file produced by
|
||||
// tools/assetbake/assetbake.py. The file's per-target byte is checked
|
||||
// tools/assetbake/assetbake.c. The file's per-target byte is checked
|
||||
// against the build platform and the loader refuses mismatches.
|
||||
//
|
||||
// outValid (optional, NULL OK) gets one flag per slot in [0, maxTiles):
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
compDescription="toolchain: BeOS R5 headers + libraries from your BeOS 5 Pro CD (beos; private media: import)"
|
||||
compGroups="toolchain:beos"
|
||||
compRequires="haikuGcc2 haikuTools"
|
||||
compHostDeps="python3"
|
||||
compHostDeps="build-tools make"
|
||||
# shellcheck source=installer/lib/beos.sh
|
||||
source "$JOEY_INSTALLER/lib/beos.sh"
|
||||
# What the sysroot is made of, as "path on the CD -> folder in sysroot".
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
compDescription="emulator: BeOS R5 VM from your BeOS 5 Pro CD (beos; private media: import)"
|
||||
compGroups="emulator:beos"
|
||||
compRequires="qemu haikuTools"
|
||||
compHostDeps="python3"
|
||||
compHostDeps="build-tools make"
|
||||
|
||||
# shellcheck source=installer/lib/beos.sh
|
||||
source "$JOEY_INSTALLER/lib/beos.sh"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
# later), the recommended tool for JoeyLib's chip-tracker music
|
||||
# (docs/Assets.adoc, "Content Creation Tools"): write the song for the TI
|
||||
# SN76489 (or the AY-3-8910 / YM2149), export it as VGM, and
|
||||
# tools/vgm2song.py turns that into a .song for tools/songbake. Its own Linux
|
||||
# tools/vgm2song turns that into a .song for tools/songbake. Its own Linux
|
||||
# x86_64 release, unpacked into toolchains/opt/furnace. Nothing builds with
|
||||
# it, so no group installs it: ./install.sh install furnace.
|
||||
# scripts/check-vgm.sh exports the SN76489 and AY demo songs it comes with
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
compDescription="emulator: Haiku R1/beta6 x86_64 VM image (haiku)"
|
||||
compGroups="emulator:haiku"
|
||||
compRequires="qemu haikuTools"
|
||||
compHostDeps="python3"
|
||||
compHostDeps=""
|
||||
haikuArch="x86_64"
|
||||
|
||||
# shellcheck source=installer/lib/haiku.sh
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
compDescription="emulator: Haiku R1/beta6 x86 gcc2h VM image (haiku32)"
|
||||
compGroups="emulator:haiku32"
|
||||
compRequires="qemu haikuTools"
|
||||
compHostDeps="python3"
|
||||
compHostDeps=""
|
||||
haikuArch="x86_gcc2"
|
||||
|
||||
# shellcheck source=installer/lib/haiku.sh
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@
|
|||
#
|
||||
# Host tools every JoeyLib setup needs: a native compiler for the in-repo
|
||||
# tools, GNU make, git with LFS (the sample assets live in LFS), the
|
||||
# archive tools the other components unpack with, Python 3 with Pillow
|
||||
# for the asset converters, and socat for joey-run's serial bridging.
|
||||
# archive tools the other components unpack with, socat for joey-run's
|
||||
# serial bridging, gawk and patch for the scripts and the IIgs build, and
|
||||
# potrace for tools/traceLogo. JoeyLib's own tools and gates are C and
|
||||
# bash; the components that build third-party code with Python list python3
|
||||
# themselves.
|
||||
|
||||
compDescription="host tools: make, host compiler, git-lfs, python3 + Pillow, archivers, socat"
|
||||
compDescription="host tools: make, host compiler, git-lfs, archivers, socat, potrace"
|
||||
# Not in any port group: every other component requires it.
|
||||
compGroups="host"
|
||||
compRequires=""
|
||||
compHostDeps="build-tools make git git-lfs curl tar xz bzip2 unzip lhasa python3 python3-pil procps diffutils socat"
|
||||
compHostDeps="build-tools make gawk patch git git-lfs curl tar xz bzip2 unzip lhasa procps diffutils socat potrace"
|
||||
compPin="1"
|
||||
|
||||
|
||||
|
|
@ -48,7 +51,7 @@ compInstall() {
|
|||
|
||||
|
||||
compInstalled() {
|
||||
command -v make >/dev/null && command -v cc >/dev/null && command -v python3 >/dev/null && command -v git-lfs >/dev/null
|
||||
command -v make >/dev/null && command -v cc >/dev/null && command -v git-lfs >/dev/null && command -v gawk >/dev/null && command -v potrace >/dev/null
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -61,7 +64,6 @@ compVerify() {
|
|||
[[ "$makeVersion" =~ ([0-9]+\.[0-9]+) ]] && makeVersion="${BASH_REMATCH[1]}"
|
||||
# Grouped targets (&:) in the port makefiles need GNU make 4.3.
|
||||
printf '%s\n4.3\n' "$makeVersion" | sort -V -C -r || { warn "GNU make $makeVersion is older than 4.3"; return 1; }
|
||||
python3 -c 'import PIL' || { warn "python3 cannot import PIL (Pillow)"; return 1; }
|
||||
stubs="$(lfsPointerFiles | wc -l)"
|
||||
[ "$stubs" = "0" ] || { warn "$stubs Git LFS files are still pointer stubs"; return 1; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# NinjaTrackerPlus: Ninjaforce's IIgs MOD replayer, shipped as Merlin
|
||||
# source plus the PHP converter tools/joeymod uses for .MOD -> .NTP.
|
||||
# source (the archive's PHP converter is not used: tools/modToNtp makes the
|
||||
# .NTP files).
|
||||
# Redistribution is granted by the copyright holder. The URL is not
|
||||
# versioned, so the pin is the archive's checksum.
|
||||
|
||||
compDescription="IIgs audio: NinjaTrackerPlus replayer source + ntpconverter.php"
|
||||
compDescription="IIgs audio: NinjaTrackerPlus replayer source"
|
||||
compGroups="toolchain:iigs"
|
||||
compRequires="host"
|
||||
compHostDeps="unzip php-cli"
|
||||
compHostDeps="unzip"
|
||||
compUrl="https://www.ninjaforce.com/downloads/ntpsources.zip"
|
||||
compSha256="461260e35dc0b10176c05865d01c4e31694f704c0c3a9e5760fc093623dc52fb"
|
||||
compPin="$compSha256"
|
||||
|
|
@ -32,7 +33,7 @@ compInstall() {
|
|||
|
||||
|
||||
compInstalled() {
|
||||
[ -f "$compDir/ninjatrackerplus.s" ] && [ -f "$compDir/ntpconverter.php" ]
|
||||
[ -f "$compDir/ninjatrackerplus.s" ]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
compDescription="emulator: Windows for Workgroups 3.11 on DOSBox (win16; private media: import)"
|
||||
compGroups="emulator:win16"
|
||||
compRequires="dosbox"
|
||||
compHostDeps="python3 xvfb"
|
||||
compHostDeps="build-tools make xvfb"
|
||||
|
||||
# shellcheck source=installer/lib/win16.sh
|
||||
source "$JOEY_INSTALLER/lib/win16.sh"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
compDescription="win16: Microsoft WinG 1.0 runtime for its Windows (optional; private: import WING10.EXE)"
|
||||
compGroups="optional"
|
||||
compRequires="win311"
|
||||
compHostDeps="python3 unzip"
|
||||
compHostDeps="build-tools make unzip"
|
||||
compPrivateFile="toolchains/firmware/win16/WING10.EXE"
|
||||
compPin="$(privateSha1For "$compPrivateFile")"
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ compInstall() {
|
|||
mkdir -p "$compDir"
|
||||
# The SDK's compressed names end in _ where the last letter was.
|
||||
for file in $wingRuntime; do
|
||||
python3 "$JOEY_REPO/tools/szddExpand.py" "$tmp/${file%?}_" "$compDir/$file" || { rm -rf "$tmp"; die "$file did not expand"; }
|
||||
"$(hostTool szddExpand)" "$tmp/${file%?}_" "$compDir/$file" || { rm -rf "$tmp"; die "$file did not expand"; }
|
||||
done
|
||||
cp "$tmp/LICENSE.TXT" "$compDir/LICENSE.TXT"
|
||||
rm -rf "$tmp"
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ unzip unzip unzip
|
|||
wget wget wget
|
||||
lhasa lhasa lhasa
|
||||
python3 python3 python3
|
||||
python3-pil python3-pil python3-pillow
|
||||
php-cli php-cli php-cli
|
||||
patch patch patch
|
||||
gawk gawk gawk
|
||||
# tools/traceLogo traces the logo art with the potrace program.
|
||||
potrace potrace potrace
|
||||
# joey-run --serial bridges some emulators' serial ports to TCP with socat.
|
||||
socat socat socat
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ BEOS_CD_FILE="toolchains/firmware/beos/beos5pro.img"
|
|||
# (the image is pinned by its checksum, so they cannot move).
|
||||
BEOS_SYSTEM_TRACK_FIRST=35109
|
||||
BEOS_SYSTEM_TRACK_SECTORS=148780
|
||||
BEOS_RAW_CD_TOOL="tools/rawCd.py"
|
||||
# The VM's disks, in the component's folder.
|
||||
BEOS_VM_SYSTEM_DISK="system.img"
|
||||
BEOS_VM_FLOPPY="floppy.img"
|
||||
|
|
@ -92,7 +91,7 @@ EOF
|
|||
|
||||
beosSystemVolume() {
|
||||
privateEnsure "$BEOS_CD_FILE"
|
||||
python3 "$JOEY_REPO/$BEOS_RAW_CD_TOOL" track "$JOEY_REPO/$BEOS_CD_FILE" "$BEOS_SYSTEM_TRACK_FIRST" "$BEOS_SYSTEM_TRACK_SECTORS" "$1" ||
|
||||
"$(hostTool rawCd)" track "$JOEY_REPO/$BEOS_CD_FILE" "$BEOS_SYSTEM_TRACK_FIRST" "$BEOS_SYSTEM_TRACK_SECTORS" "$1" ||
|
||||
die "could not read the x86 track of $BEOS_CD_FILE"
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +110,7 @@ beosVmPrepare() {
|
|||
rm -rf "$part"
|
||||
mkdir -p "$part"
|
||||
beosSystemVolume "$part/$BEOS_VM_SYSTEM_DISK"
|
||||
python3 "$JOEY_REPO/$BEOS_RAW_CD_TOOL" eltorito "$JOEY_REPO/$BEOS_CD_FILE" "$part/$BEOS_VM_FLOPPY" ||
|
||||
"$(hostTool rawCd)" eltorito "$JOEY_REPO/$BEOS_CD_FILE" "$part/$BEOS_VM_FLOPPY" ||
|
||||
die "could not read the boot floppy image of $BEOS_CD_FILE"
|
||||
beosJobRunner > "$part/UserBootscript"
|
||||
printf '%s\n' "$BEOS_KERNEL_SETTINGS" > "$part/kernel"
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ importPaths() {
|
|||
*.adf|*.ADF)
|
||||
rm -rf "$tmp/adf"
|
||||
mkdir -p "$tmp/adf"
|
||||
if python3 "$JOEY_REPO/tools/adfTool.py" extractall "$file" "$tmp/adf" 2>/dev/null; then
|
||||
if "$(hostTool adfTool)" extractall "$file" "$tmp/adf" 2>/dev/null; then
|
||||
find "$tmp/adf" -type f -print0 | while IFS= read -r -d '' inner; do
|
||||
importOneFile "$inner"
|
||||
done
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ The upstream file changed. Check it, then update the pin in the component file."
|
|||
fetchOciBlob() {
|
||||
local token=""
|
||||
if [ "$(sha256Of "$JOEY_CACHE/$3" 2>/dev/null || true)" != "$2" ]; then
|
||||
token="$(curl -fsSL "https://ghcr.io/token?scope=repository:$1:pull" | python3 -c 'import json, sys; print(json.load(sys.stdin)["token"])')" \
|
||||
|| die "cannot get a registry token for $1"
|
||||
token="$(curl -fsSL "https://ghcr.io/token?scope=repository:$1:pull" | sed -n 's/.*"token" *: *"\([^"]*\)".*/\1/p')" \
|
||||
&& [ -n "$token" ] || die "cannot get a registry token for $1"
|
||||
fi
|
||||
fetchFile "https://ghcr.io/v2/$1/blobs/sha256:$2" "$2" "$3" "Authorization: Bearer $token"
|
||||
}
|
||||
|
|
@ -199,6 +199,15 @@ freshBuildDir() {
|
|||
}
|
||||
|
||||
|
||||
# hostTool NAME -- print the path of one of JoeyLib's host tools
|
||||
# (make/tools.mk's, in build/tools/), built first if it is not current; a
|
||||
# component that runs one lists "build-tools make" among its compHostDeps.
|
||||
hostTool() {
|
||||
make -s -f "$JOEY_REPO/make/tools.mk" "$1" > /dev/null || die "cannot build tools/$1 (the host component installs make and a C compiler)"
|
||||
printf '%s\n' "${JOEY_BUILD:-$JOEY_REPO/build}/tools/$1"
|
||||
}
|
||||
|
||||
|
||||
# dropBuildDir ID -- delete ID's scratch tree unless --keep-build.
|
||||
dropBuildDir() {
|
||||
if [ "$JOEY_KEEP_BUILD" = "1" ]; then
|
||||
|
|
|
|||
|
|
@ -75,20 +75,17 @@ haikuBfsNew() {
|
|||
|
||||
|
||||
# haikuBootOffset IMAGE -- the byte offset of an anyboot image's first
|
||||
# partition, its BFS system volume (from the MBR: entry 1's start sector).
|
||||
# partition, its BFS system volume (from the MBR: entry 1, at byte 446,
|
||||
# holds its start sector, little-endian, at byte 8).
|
||||
haikuBootOffset() {
|
||||
python3 - "$1" <<'PY'
|
||||
import struct
|
||||
import sys
|
||||
local mbrEntry1=446
|
||||
local entryStartLba=8
|
||||
local sectorBytes=512
|
||||
local b
|
||||
|
||||
MBR_ENTRY_1 = 446
|
||||
ENTRY_START_LBA = 8
|
||||
SECTOR_BYTES = 512
|
||||
|
||||
with open(sys.argv[1], 'rb') as image:
|
||||
mbr = image.read(SECTOR_BYTES)
|
||||
print(struct.unpack_from('<I', mbr, MBR_ENTRY_1 + ENTRY_START_LBA)[0] * SECTOR_BYTES)
|
||||
PY
|
||||
read -ra b <<< "$(od -An -tu1 -v -j $((mbrEntry1 + entryStartLba)) -N 4 "$1")"
|
||||
[ "${#b[@]}" -eq 4 ] || return 1
|
||||
echo $(((b[0] | b[1] << 8 | b[2] << 16 | b[3] << 24) * sectorBytes))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ WIN16_FIRMWARE="toolchains/firmware/win16"
|
|||
WIN16_ISO="$WIN16_FIRMWARE/wfw311.iso"
|
||||
WIN16_S3_FILES="S3TRIO.DRV VDDS3764.386 VGA_ENG.3GR"
|
||||
WIN16_DOSBOX_CONF="scripts/dosbox-win16.conf"
|
||||
WIN16_INI_TOOL="tools/winIni.py"
|
||||
# The INI editor (tools/winIni): the image is remade when its source changes.
|
||||
WIN16_INI_SOURCE="tools/winIni/winIni.c"
|
||||
# Setup takes about 20 s at full speed.
|
||||
WIN16_SETUP_SECONDS=300
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ win16Configure() {
|
|||
for name in $WIN16_S3_FILES; do
|
||||
cp "$JOEY_REPO/$WIN16_FIRMWARE/s3/$name" "$windows/SYSTEM/$name"
|
||||
done
|
||||
python3 "$JOEY_REPO/$WIN16_INI_TOOL" "$windows/SYSTEM.INI" \
|
||||
"$(hostTool winIni)" "$windows/SYSTEM.INI" \
|
||||
set boot display.drv s3trio.drv \
|
||||
set boot 386grabber vga_eng.3gr \
|
||||
set boot.description display.drv "S3 Trio64V 1.70.04 800x600 256 C SF" \
|
||||
|
|
@ -84,7 +85,7 @@ win16Pin() {
|
|||
privateSha1For "$file"
|
||||
done
|
||||
declare -f win16Configure win16Prepare win16Setup win16SetupAnswers
|
||||
cat "$JOEY_REPO/$WIN16_DOSBOX_CONF" "$JOEY_REPO/$WIN16_INI_TOOL"
|
||||
cat "$JOEY_REPO/$WIN16_DOSBOX_CONF" "$JOEY_REPO/$WIN16_INI_SOURCE"
|
||||
} | sha256sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
|
|
|
|||
21
joey-run
21
joey-run
|
|
@ -415,21 +415,12 @@ runMame() {
|
|||
|
||||
# ---- the serial link ----------------------------------------------------------
|
||||
|
||||
# portFree PORT -- nothing on this machine listens on TCP PORT yet. Checked
|
||||
# up front because an emulator that cannot take the port only says so in a
|
||||
# log nobody reads, and runs with the serial port unplugged.
|
||||
# portFree PORT -- nothing on this machine listens on TCP PORT yet: a
|
||||
# connection to it here is refused. Checked up front because an emulator
|
||||
# that cannot take the port only says so in a log nobody reads, and runs
|
||||
# with the serial port unplugged.
|
||||
portFree() {
|
||||
python3 - "$1" <<'PY'
|
||||
import socket
|
||||
import sys
|
||||
|
||||
probe = socket.socket()
|
||||
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
probe.bind(("", int(sys.argv[1])))
|
||||
except OSError:
|
||||
sys.exit(1)
|
||||
PY
|
||||
! (exec 3<> "/dev/tcp/127.0.0.1/$1") 2> /dev/null
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -486,7 +477,7 @@ mameSerial() {
|
|||
local bitb="socket.$serialHost:$serialPort"
|
||||
[ -n "$serialMode" ] || return 0
|
||||
mkdir -p "$work/cfg"
|
||||
python3 "$repo/scripts/lib/mameNullModemCfg.py" "$driver" ":$slot:null_modem" "$serialBaud" \
|
||||
"$repo/scripts/lib/mameNullModemCfg.sh" "$driver" ":$slot:null_modem" "$serialBaud" \
|
||||
> "$work/cfg/$driver.cfg" || fail "--serial-baud $serialBaud is not a rate MAME's null modem has"
|
||||
if [ "$serialMode" = "listen" ]; then
|
||||
relaySerial "unix-listen:serial.sock" "$work/serial.sock"
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ $(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB)
|
|||
$(AMIGA_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) -o $$@ $(LDFLAGS) $(call programVar,$(1),LDFLAGS)
|
||||
-include $(call programBin,$(1)).d
|
||||
|
||||
$(call programBin,$(1)).info: $(call programIcon,$(1)) $(ICON_BAKE_DEPS)
|
||||
$(call programBin,$(1)).info: $(call programIcon,$(1)) $(ICON_BAKE)
|
||||
@mkdir -p $$(dir $$@)
|
||||
$(ICON_BAKE) info $$< $$@ $(AMIGA_ICON_STACK)
|
||||
endef
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ HOSTED_SDK_CFLAGS :=
|
|||
HOSTED_LINK := -lbe -lmedia -ldevice $(BE_CXX_RUNTIME)
|
||||
PROGRAM_EXT :=
|
||||
# Its icons, the BeOS kind Haiku shows too: 32x32 and 16x16 in the BeOS
|
||||
# palette, raw (tools/iconBake.py be), put into the linked program's
|
||||
# palette, raw (tools/iconBake be), put into the linked program's
|
||||
# resources by xres (component haikuTools) as BEOS:L:STD_ICON and
|
||||
# BEOS:M:STD_ICON, BE_ICON_ID each; and the 32x32 one as Haiku's vector
|
||||
# icon, BEOS:ICON, which Haiku draws at any size (BeOS R5 ignores it). An
|
||||
|
|
|
|||
|
|
@ -182,13 +182,18 @@ programOptionalDir = $(if $(PROGRAM_OPTIONAL_APART),$(PROGRAM_BUILD)/data/$(call
|
|||
# $(call programIconDir,ID), where the port's bakes of it go, by its name (the
|
||||
# IIgs disk scripts look icons up by it), or by its id for a program built
|
||||
# apart (ID_BINDIR: a probe build may share a shipped program's name).
|
||||
# ICON_BAKE is the baker (tools/iconBake.py says what each port gets); a rule
|
||||
# that runs it depends on ICON_BAKE_DEPS too, so a change to the baker
|
||||
# rebakes.
|
||||
programIcon = $(or $(call programVar,$(1),ICON),$(REPO_DIR)/assets/joeyIcon.png)
|
||||
programIconDir = $(PROGRAM_BUILD)/icon/$(if $(call programVar,$(1),BINDIR),$(1),$(call programName,$(1)))
|
||||
ICON_BAKE := python3 $(REPO_DIR)/tools/iconBake.py
|
||||
ICON_BAKE_DEPS := $(REPO_DIR)/tools/iconBake.py
|
||||
|
||||
# The host bakers a port's rules run, built by make/tools.mk (make/programs.mk
|
||||
# hands their builds to it): ASSET_BAKE (tools/assetbake, indexed PNGs into
|
||||
# tile banks and sprite cels), ICON_BAKE (tools/iconBake, a program's icon
|
||||
# for each system; its usage says what each port gets), and MOD_TO_NTP
|
||||
# (tools/modToNtp, a .MOD as the IIgs's .NTP, which tools/joeymod runs). A
|
||||
# rule that runs one depends on it too, so a change to the baker rebakes.
|
||||
ASSET_BAKE := $(JOEY_BUILD)/tools/assetbake
|
||||
ICON_BAKE := $(JOEY_BUILD)/tools/iconBake
|
||||
MOD_TO_NTP := $(JOEY_BUILD)/tools/modToNtp
|
||||
|
||||
|
||||
# Rules that hand a host tool's build to make/tools.mk depend on FORCE, so
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ app-$(2): $(LIB) $(call programVar,$(1),SRCS) $(PORT_ENTRY) $(call programVar,$(
|
|||
|
||||
# Its icon, the one Program Manager shows: icon32 among its resources, a
|
||||
# 32x32 BMP on DVX's grey, which sdk.mk takes from the project's folder.
|
||||
$(APPSDIR)/$(2)/icon32.bmp: $(call programIcon,$(1)) $(ICON_BAKE_DEPS)
|
||||
$(APPSDIR)/$(2)/icon32.bmp: $(call programIcon,$(1)) $(ICON_BAKE)
|
||||
@mkdir -p $$(dir $$@)
|
||||
$(ICON_BAKE) bmp $$< $$@
|
||||
endef
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#
|
||||
# Worth knowing which ones the gates use:
|
||||
# UBER the golden-hash vehicle: every public op, a hash per op, which
|
||||
# tools/diff-uber-hashes compares against tests/goldens/uber (the
|
||||
# build/tools/diffUberHashes compares against tests/goldens/uber (the
|
||||
# X68000 against the IIgs's, DVX against the DOS port's: the same
|
||||
# stage). UBERPROB is UBER with its UBER_PROBE rows.
|
||||
# PATTERN the SCB acceptance vehicle: 8 bands of 25 lines with 8 gradient
|
||||
|
|
@ -208,7 +208,7 @@ AUDPAR_IIGS_BINDIR := $(PROGRAM_BUILD)/probe/bin
|
|||
AUDPAR_DATA := $(call programDataDir,AUDPAR)/test.mod
|
||||
AUDPAR_IIGS_DATA := $(call programDataDir,AUDPAR)/TEST.NTP
|
||||
|
||||
$(call programDataDir,AUDPAR)/TEST.NTP: $(REPO_DIR)/assets/test.mod $(REPO_DIR)/tools/joeymod/joeymod
|
||||
$(call programDataDir,AUDPAR)/TEST.NTP: $(REPO_DIR)/assets/test.mod $(REPO_DIR)/tools/joeymod/joeymod $(MOD_TO_NTP)
|
||||
@mkdir -p $(dir $@)
|
||||
$(REPO_DIR)/tools/joeymod/joeymod $< $@
|
||||
|
||||
|
|
@ -230,9 +230,13 @@ $(call programDataDir,AUDIO)/%: $(REPO_DIR)/assets/%
|
|||
# DATA/tilebank holds this port's bake of it, every other target's bake (each
|
||||
# must be refused) and the expected pixels.
|
||||
TILEBANK_PNG := $(REPO_DIR)/tests/host/tileBank.png
|
||||
TILEBANK_STAGE := $(REPO_DIR)/tests/host/tileBankStage.py
|
||||
TILEBANK_BAKE_DEPS := $(TILEBANK_PNG) $(TILEBANK_STAGE) $(REPO_DIR)/tools/assetbake/assetbake.py
|
||||
TILEBANK_STAGE := $(REPO_DIR)/tests/host/tileBankStage.sh
|
||||
TILEBANK_EXPECT := $(JOEY_BUILD)/tools/tileBankExpect
|
||||
TILEBANK_BAKE_DEPS := $(TILEBANK_PNG) $(TILEBANK_STAGE) $(ASSET_BAKE) $(TILEBANK_EXPECT)
|
||||
TILEBANK_DATA := $(call programDataDir,TILEBANK)/tilebank/own.tbk
|
||||
|
||||
$(TILEBANK_EXPECT): FORCE
|
||||
$(MAKE) -f $(REPO_DIR)/make/tools.mk $(notdir $@)
|
||||
|
||||
$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS)
|
||||
python3 $(TILEBANK_STAGE) $(ASSET_PLATFORM) $(TILEBANK_PNG) $(dir $@)
|
||||
$(TILEBANK_STAGE) $(ASSET_PLATFORM) $(TILEBANK_PNG) $(dir $@)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
# HOSTED_LINK_OUT, the file one program is linked to, and HOSTED_PACK, the
|
||||
# command that packs it into the program file ($(1) the id in both).
|
||||
# A port whose programs show an icon (make/common.mk's programIcon, baked by
|
||||
# tools/iconBake.py) sets HOSTED_ICON, the file one program's icon is baked
|
||||
# tools/iconBake) sets HOSTED_ICON, the file one program's icon is baked
|
||||
# to, and HOSTED_ICON_BAKE, the command that bakes it there from the
|
||||
# program's picture; the program is rebuilt after it, and HOSTED_ICON_LINK
|
||||
# is what of it the link takes (a resource object, a resource script), or
|
||||
|
|
@ -160,14 +160,14 @@ $(if $(call HOSTED_ICON,$(1)),$(call hostedIconRules,$(1)))
|
|||
endef
|
||||
|
||||
# $(call hostedIconScript,ID,FORMAT): a Windows port's bake: the program's
|
||||
# icon in iconBake.py's FORMAT (ico, ico16) as icon.ico in its icon folder,
|
||||
# icon in iconBake's FORMAT (ico, ico16) as icon.ico in its icon folder,
|
||||
# and icon.rc, the resource script that gives it the number HOSTED_ICON_ID
|
||||
# (by its whole path, so the resource compiler may run anywhere).
|
||||
hostedIconScript = $(ICON_BAKE) $(2) $(call programIcon,$(1)) $(call programIconDir,$(1))/icon.ico && printf '%s ICON "%s"\n' $(HOSTED_ICON_ID) $(call programIconDir,$(1))/icon.ico > $(call programIconDir,$(1))/icon.rc
|
||||
|
||||
# One program's icon, $(1) its id, baked by the port's command.
|
||||
define hostedIconRules
|
||||
$(call HOSTED_ICON,$(1)): $(call programIcon,$(1)) $(ICON_BAKE_DEPS)
|
||||
$(call HOSTED_ICON,$(1)): $(call programIcon,$(1)) $(ICON_BAKE)
|
||||
@mkdir -p $$(dir $$@)
|
||||
$(call HOSTED_ICON_BAKE,$(1))
|
||||
endef
|
||||
|
|
|
|||
11
make/iigs.mk
11
make/iigs.mk
|
|
@ -60,15 +60,16 @@ IIGS_LIB_HDRS := $(wildcard $(INCLUDE_DIR)/joey/*.h) $(wildcard $(SRC_CORE)/*.h)
|
|||
# binary that ships next to the program; jlpAudioInit loads "ntpplayer" from
|
||||
# disk at run time (src/iigs/audio_full.c NTP_PATH), so every disk a program
|
||||
# goes on carries it (make-iigs-disk.sh). The source is assembled
|
||||
# with JoeyLib's interrupt window added to NTPstreamsound (NTP_PATCH).
|
||||
# with JoeyLib's interrupt window added to NTPstreamsound (NTP_PATCH, applied
|
||||
# exactly: a hunk that does not apply fails the build).
|
||||
NTP_SRC := $(IIGS_NTP_DIR)/ninjatrackerplus.s
|
||||
NTP_PATCH := $(REPO_DIR)/make/support/iigs/ntpIrqWindow.py
|
||||
NTP_PATCH := $(REPO_DIR)/make/support/iigs/ntpIrqWindow.patch
|
||||
NTP_BIN := $(BUILD)/audio/ntpplayer.bin
|
||||
IIGS_MERLIN := $(IIGS_AS)
|
||||
|
||||
$(NTP_BIN): $(NTP_SRC) $(NTP_PATCH) $(IIGS_MERLIN)
|
||||
@mkdir -p $(BUILD)/audio
|
||||
@python3 $(NTP_PATCH) $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
|
||||
patch --quiet --fuzz=0 --no-backup-if-mismatch -r - -o $(BUILD)/audio/ninjatrackerplus.s $(NTP_SRC) $(NTP_PATCH)
|
||||
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
|
||||
mv $(BUILD)/audio/ntpplayer $@
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ programIconFile = $(call programIconDir,$(1))/finder.icons
|
|||
programTarget = $(call programBin,$(1)) $(call programIconFile,$(1))
|
||||
|
||||
# One program, $(1) its id: clang-build.sh compiles, links and wraps it, and
|
||||
# writes one header-dependency file for all its sources; iconBake.py bakes
|
||||
# writes one header-dependency file for all its sources; iconBake bakes
|
||||
# its Finder icon. ID_IIGS_OVERFLOW_CAP
|
||||
# opts into large overflow segments (see clang-build.sh). NTP rides on every
|
||||
# disk a program goes on, so it is built first.
|
||||
|
|
@ -127,7 +128,7 @@ $(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(IIGS_CLANG_BUILD)
|
|||
@mkdir -p $$(dir $$@) $(DEP_DIR)
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/$(1).d $(call programVar,$(1),CFLAGS) $(INCLUDES) -o $$@ $(call programVar,$(1),SRCS) $(LIB)
|
||||
|
||||
$(call programIconFile,$(1)): $(call programIcon,$(1)) $(ICON_BAKE_DEPS)
|
||||
$(call programIconFile,$(1)): $(call programIcon,$(1)) $(ICON_BAKE)
|
||||
@mkdir -p $$(dir $$@)
|
||||
$(ICON_BAKE) iigs $$< $$@ $(call programName,$(1))
|
||||
endef
|
||||
|
|
|
|||
|
|
@ -70,9 +70,10 @@ programs: $(foreach p,$(PORT_PROGRAMS),$(call programTarget,$(p)) $(call program
|
|||
.PHONY: $(addprefix program-,$(PORT_PROGRAMS) $(PORT_TEST_PROGRAMS))
|
||||
$(foreach p,$(PORT_PROGRAMS) $(PORT_TEST_PROGRAMS),$(eval program-$(p): $(call programTarget,$(p)) $(call programVar,$(p),DATA)))
|
||||
|
||||
# The host sprite pre-compiler a description's bakes use; make/tools.mk owns
|
||||
# its sources.
|
||||
$(SPRITEBAKE): FORCE
|
||||
# The host bakers a description's bakes use (the sprite pre-compiler, the
|
||||
# asset and icon bakers, the .NTP converter); make/tools.mk owns their
|
||||
# sources.
|
||||
$(SPRITEBAKE) $(ASSET_BAKE) $(ICON_BAKE) $(MOD_TO_NTP): FORCE
|
||||
$(MAKE) -f $(REPO_DIR)/make/tools.mk $(notdir $@)
|
||||
|
||||
# A project's goal for one port: its programs, their DATA and the packages
|
||||
|
|
|
|||
108
make/support/iigs/ntpIrqWindow.patch
Normal file
108
make/support/iigs/ntpIrqWindow.patch
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
See LICENSE.md for license information.
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
ntpIrqWindow.patch - bound NinjaTrackerPlus's interrupts-off stretches.
|
||||
make/iigs.mk applies it to the replayer's source with patch --fuzz=0 before
|
||||
Merlin32 assembles it.
|
||||
|
||||
NinjaTrackerPlus moves sample data into the DOC with interrupts masked,
|
||||
~4 us a byte (measured under MAME), and the Z8530 SCC's receive FIFO only
|
||||
lasts ~3.1 ms at 9600 baud (~2.3 ms under MAME). Two stretches outlast it:
|
||||
- NTPstreamsound fills a stream's 512-byte DOC page with two unrolled
|
||||
256-byte copies under its own SEI: ~2.4 ms. stream_window goes between
|
||||
the halves: pending interrupts run there, then the DOC's mode and
|
||||
address are put back for the second half. The stream's oscillators are
|
||||
already halted, and its job is cleared first, so a refill interrupt left
|
||||
pending by the slot's previous sound cannot stream into the page.
|
||||
- The DOC interrupt handler refills every streaming oscillator that is
|
||||
due before it returns, 256 bytes each: ~1 ms apiece, ~2.5 ms for two.
|
||||
It returns after one instead; the DOC keeps its IRQ up for the rest, so
|
||||
the handler is re-entered at once, after anything the ROM's interrupt
|
||||
manager puts first (the SCC is on its fast path).
|
||||
Both only while the word at offset 27 of the replayer (irq_window, right
|
||||
after the documented jump table) is nonzero. The word after it, at offset
|
||||
29 (playing_addr), holds the address of the replayer's playing flag, so
|
||||
audio_full.c can see a song that does not loop come to its end. audio_full.c
|
||||
sets it from hal.c's tight interrupt-latency bound before every
|
||||
NTPstreamsound, so with the bound loose the replayer does exactly what it
|
||||
always did.
|
||||
|
||||
The source is the installer's pinned ntpsources.zip (checksummed), so every
|
||||
hunk applies exactly; a hunk that does not means NTP changed and this patch
|
||||
must be revisited, so the build fails instead of guessing.
|
||||
|
||||
--- ninjatrackerplus.s
|
||||
+++ ninjatrackerplus.s
|
||||
@@ -138,6 +138,14 @@
|
||||
; 1 byte channel (0-7 placed in the upper 4 bits, example: $00 for left, $10 for right)
|
||||
jmp streamsound
|
||||
|
||||
+; irq_window (JoeyLib, offset 27): nonzero bounds the interrupts-off
|
||||
+; stretches -- see stream_window and interrupt_handler_exit.
|
||||
+irq_window dw 0
|
||||
+
|
||||
+; playing_addr (JoeyLib, offset 29): where the replayer keeps its playing
|
||||
+; flag (0 once a song played once has ended), for jlAudioIsPlayingMod.
|
||||
+playing_addr dw playing
|
||||
+
|
||||
*-------------------------------
|
||||
|
||||
; to stay out of the way, do not use the direct page at all
|
||||
@@ -1301,6 +1309,7 @@
|
||||
|
||||
ldy streamsound_osc2
|
||||
jsr stream_copy ;fill buffer with 512 bytes for first play
|
||||
+ jsr stream_window ;JoeyLib: interrupts in between the halves, if asked
|
||||
jsr stream_copy_no_init
|
||||
|
||||
mx %00 ;we're in 16bit mode on return
|
||||
@@ -1383,6 +1392,31 @@
|
||||
clc
|
||||
rtl
|
||||
|
||||
+; stream_window (JoeyLib): between the two halves of streamsound's first
|
||||
+; DOC fill, when irq_window is nonzero, let pending interrupts run, then
|
||||
+; point the DOC back at the second half (a handler may have used it).
|
||||
+; In and out: mx %00, Y = the stream's track * 2.
|
||||
+ mx %00
|
||||
+stream_window = *
|
||||
+ lda irq_window
|
||||
+ beq stream_window_1
|
||||
+ sep #$20
|
||||
+ mx %10
|
||||
+ ldx streamsound_osc ;no refill job left pending from the slot's last sound
|
||||
+ stz oscillator_job_tbl,x
|
||||
+ cli
|
||||
+ nop ;a pending interrupt is taken here
|
||||
+ sei
|
||||
+ jsr access_doc_ram
|
||||
+ rep #$20
|
||||
+ mx %00
|
||||
+ lda stream_doc_page,y ;the page's second half
|
||||
+ inc
|
||||
+ xba
|
||||
+ sta sound_address
|
||||
+stream_window_1 = *
|
||||
+ rts
|
||||
+
|
||||
mx %10
|
||||
streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
|
||||
ldy streamsound_osc_count
|
||||
@@ -1454,6 +1488,8 @@
|
||||
ldx interrupt_job
|
||||
jsr (interrupt_jump_tbl,x)
|
||||
|
||||
+ lda irq_window ;JoeyLib: one oscillator an interrupt when asked;
|
||||
+ bne interrupt_handler_exit ;the DOC keeps its IRQ up for any others
|
||||
]loop lda sound_control
|
||||
bmi ]loop
|
||||
|
||||
@@ -1465,6 +1501,7 @@
|
||||
lda sound_data
|
||||
bpl interrupt_handler_loop
|
||||
|
||||
+interrupt_handler_exit = *
|
||||
sep #$30
|
||||
pld
|
||||
plb
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# ntpIrqWindow.py SRC DST - bound NinjaTrackerPlus's interrupts-off stretches.
|
||||
#
|
||||
# NinjaTrackerPlus moves sample data into the DOC with interrupts masked,
|
||||
# ~4 us a byte (measured under MAME), and the Z8530 SCC's receive FIFO only
|
||||
# lasts ~3.1 ms at 9600 baud (~2.3 ms under MAME). Two stretches outlast it:
|
||||
# - NTPstreamsound fills a stream's 512-byte DOC page with two unrolled
|
||||
# 256-byte copies under its own SEI: ~2.4 ms. stream_window goes between
|
||||
# the halves: pending interrupts run there, then the DOC's mode and
|
||||
# address are put back for the second half. The stream's oscillators are
|
||||
# already halted, and its job is cleared first, so a refill interrupt left
|
||||
# pending by the slot's previous sound cannot stream into the page.
|
||||
# - The DOC interrupt handler refills every streaming oscillator that is
|
||||
# due before it returns, 256 bytes each: ~1 ms apiece, ~2.5 ms for two.
|
||||
# It returns after one instead; the DOC keeps its IRQ up for the rest, so
|
||||
# the handler is re-entered at once, after anything the ROM's interrupt
|
||||
# manager puts first (the SCC is on its fast path).
|
||||
# Both only while the word at offset 27 of the replayer (irq_window, right
|
||||
# after the documented jump table) is nonzero. The word after it, at offset
|
||||
# 29 (playing_addr), holds the address of the replayer's playing flag, so
|
||||
# audio_full.c can see a song that does not loop come to its end. audio_full.c sets it from
|
||||
# hal.c's tight interrupt-latency bound before every NTPstreamsound, so with
|
||||
# the bound loose the replayer does exactly what it always did.
|
||||
#
|
||||
# The source is the installer's pinned ntpsources.zip (checksummed), so
|
||||
# every anchor below is exact; a miss means NTP changed and this script
|
||||
# must be revisited, so it fails the build instead of guessing.
|
||||
import sys
|
||||
|
||||
TABLE_END = """ jmp streamsound
|
||||
"""
|
||||
TABLE_END_NEW = """ jmp streamsound
|
||||
|
||||
; irq_window (JoeyLib, offset 27): nonzero bounds the interrupts-off
|
||||
; stretches -- see stream_window and interrupt_handler_exit.
|
||||
irq_window dw 0
|
||||
|
||||
; playing_addr (JoeyLib, offset 29): where the replayer keeps its playing
|
||||
; flag (0 once a song played once has ended), for jlAudioIsPlayingMod.
|
||||
playing_addr dw playing
|
||||
"""
|
||||
FILL = """ ldy streamsound_osc2
|
||||
jsr stream_copy ;fill buffer with 512 bytes for first play
|
||||
jsr stream_copy_no_init
|
||||
"""
|
||||
FILL_NEW = """ ldy streamsound_osc2
|
||||
jsr stream_copy ;fill buffer with 512 bytes for first play
|
||||
jsr stream_window ;JoeyLib: interrupts in between the halves, if asked
|
||||
jsr stream_copy_no_init
|
||||
"""
|
||||
COPY_HELPERS = """ mx %10
|
||||
streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
|
||||
"""
|
||||
COPY_HELPERS_NEW = """; stream_window (JoeyLib): between the two halves of streamsound's first
|
||||
; DOC fill, when irq_window is nonzero, let pending interrupts run, then
|
||||
; point the DOC back at the second half (a handler may have used it).
|
||||
; In and out: mx %00, Y = the stream's track * 2.
|
||||
mx %00
|
||||
stream_window = *
|
||||
lda irq_window
|
||||
beq stream_window_1
|
||||
sep #$20
|
||||
mx %10
|
||||
ldx streamsound_osc ;no refill job left pending from the slot's last sound
|
||||
stz oscillator_job_tbl,x
|
||||
cli
|
||||
nop ;a pending interrupt is taken here
|
||||
sei
|
||||
jsr access_doc_ram
|
||||
rep #$20
|
||||
mx %00
|
||||
lda stream_doc_page,y ;the page's second half
|
||||
inc
|
||||
xba
|
||||
sta sound_address
|
||||
stream_window_1 = *
|
||||
rts
|
||||
|
||||
mx %10
|
||||
streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
|
||||
"""
|
||||
HANDLER_JOB = """ ldx interrupt_job
|
||||
jsr (interrupt_jump_tbl,x)
|
||||
|
||||
]loop lda sound_control
|
||||
"""
|
||||
HANDLER_JOB_NEW = """ ldx interrupt_job
|
||||
jsr (interrupt_jump_tbl,x)
|
||||
|
||||
lda irq_window ;JoeyLib: one oscillator an interrupt when asked;
|
||||
bne interrupt_handler_exit ;the DOC keeps its IRQ up for any others
|
||||
]loop lda sound_control
|
||||
"""
|
||||
HANDLER_END = """ bpl interrupt_handler_loop
|
||||
|
||||
sep #$30
|
||||
"""
|
||||
HANDLER_END_NEW = """ bpl interrupt_handler_loop
|
||||
|
||||
interrupt_handler_exit = *
|
||||
sep #$30
|
||||
"""
|
||||
|
||||
|
||||
def patchOnce(text, old, new):
|
||||
if text.count(old) != 1:
|
||||
sys.exit("ntpIrqWindow: anchor not found exactly once -- NinjaTrackerPlus changed:\n" + old)
|
||||
return text.replace(old, new)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit("usage: ntpIrqWindow.py SRC DST")
|
||||
with open(sys.argv[1], "r", encoding="ascii") as f:
|
||||
text = f.read()
|
||||
text = patchOnce(text, TABLE_END, TABLE_END_NEW)
|
||||
text = patchOnce(text, FILL, FILL_NEW)
|
||||
text = patchOnce(text, COPY_HELPERS, COPY_HELPERS_NEW)
|
||||
text = patchOnce(text, HANDLER_JOB, HANDLER_JOB_NEW)
|
||||
text = patchOnce(text, HANDLER_END, HANDLER_END_NEW)
|
||||
with open(sys.argv[2], "w", encoding="ascii") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
main()
|
||||
|
|
@ -6,20 +6,60 @@
|
|||
#
|
||||
# These build with the host's default `cc` (no cross-toolchain) into
|
||||
# build/tools/ (JOEY_BUILD's, make/common.mk). Currently:
|
||||
# adfTool -- read files out of an Amiga disk image (.adf): the
|
||||
# installer's import.
|
||||
# assetbake -- indexed PNG -> a target's tile bank (.tbk) or sprite
|
||||
# cels (.spr).
|
||||
# diffUberHashes -- two UBER logs compared op by op by surface hash: the
|
||||
# golden gates' check against tests/goldens/uber.
|
||||
# diffUberPerf -- two UBER logs compared op by op by ops/sec, the worst
|
||||
# gaps against a threshold first.
|
||||
# iconBake -- one picture -> each system's program icon.
|
||||
# modloopend -- inject an E8F stop-marker pattern into a .MOD ahead
|
||||
# of time (the Amiga audio HAL marks a song played with
|
||||
# loop false itself). joeymod invokes it for .amod output.
|
||||
# modToNtp -- a .MOD -> a NinjaTrackerPlus song (.NTP), the IIgs
|
||||
# replayer's: joeymod invokes it for .ntp output.
|
||||
# rawCd -- a raw CD image's track, or its El Torito boot floppy:
|
||||
# the installer's BeOS VM.
|
||||
# scanPtrBug -- find the w65816 compiler's far-pointer bug (a pointer's
|
||||
# bank in its offset half) in its assembly output.
|
||||
# songbake -- chip-tracker text notation (.song) -> a JYM1 stream.
|
||||
# spritebake -- the sprite pre-compiler, one per target (below).
|
||||
#
|
||||
# Asset baking (PNG -> .tbk / .spr) is handled by tools/assetbake/
|
||||
# assetbake.py (Python). It is invoked directly from each platform's
|
||||
# Makefile and does not need a C build step.
|
||||
# szddExpand -- expand a COMPRESS.EXE (SZDD) file: the installer's WinG.
|
||||
# tiled2joey -- a Tiled map (TMX or TMJ) -> a tile sheet and maps.
|
||||
# tileBankExpect -- the tile-bank gates' expected pixels
|
||||
# (tests/host/tileBankStage.sh).
|
||||
# traceLogo -- a flat-colour PNG logo -> a layered SVG, through the
|
||||
# potrace program: remakes assets/joeylibLogo.svg.
|
||||
# uberPerfTable -- every port's UBER log -> one Markdown table of ops/sec
|
||||
# against a reference port.
|
||||
# vgm2song -- a tracker's VGM export (Furnace) -> a .song.
|
||||
# winIni -- edit a Windows 3.x .INI file: the installer's Windows 3.11
|
||||
# and the Win16 gates.
|
||||
# xdftool -- read and write Human68k floppy images (.XDF): joey-run
|
||||
# and the X68000 gates stage programs with it.
|
||||
|
||||
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
|
||||
|
||||
BUILD_DIR := $(JOEY_BUILD)/tools
|
||||
TOOLS_DIR := $(REPO_DIR)/tools
|
||||
|
||||
# The tools of one C file (and any headers beside it) over tools/common (PNG,
|
||||
# DEFLATE, whole files, byte buffers): tools/<name>/<name>.c, or a test's
|
||||
# helper in tests/host.
|
||||
COMMON_TOOLS := adfTool assetbake diffUberHashes diffUberPerf iconBake modToNtp rawCd scanPtrBug songbake szddExpand tiled2joey traceLogo uberPerfTable vgm2song winIni xdftool
|
||||
TOOL_COMMON_SRC := $(wildcard $(TOOLS_DIR)/common/*.c)
|
||||
TOOL_COMMON_HDRS := $(wildcard $(TOOLS_DIR)/common/*.h)
|
||||
|
||||
# The gates' helpers (scripts/verify-*.sh and friends, which run them through
|
||||
# joeyTool): tests/gate/<name>.c, each over tools/common and the gates' own
|
||||
# shared code in tests/gate/common.
|
||||
GATE_DIR := $(REPO_DIR)/tests/gate
|
||||
GATE_TOOLS := $(basename $(notdir $(wildcard $(GATE_DIR)/*.c)))
|
||||
GATE_COMMON_SRC := $(wildcard $(GATE_DIR)/common/*.c)
|
||||
GATE_COMMON_HDRS := $(wildcard $(GATE_DIR)/common/*.h)
|
||||
|
||||
MODLOOPEND_SRC := $(TOOLS_DIR)/modloopend/modloopend.c $(SRC_CORE)/modStopMarker.c
|
||||
MODLOOPEND_BIN := $(BUILD_DIR)/modloopend
|
||||
|
||||
|
|
@ -35,16 +75,37 @@ SPRITEBAKE_INC := -I$(INCLUDE_DIR) -I$(SRC_CORE) -I$(SRC_CG)
|
|||
# SPC_MAGIC must rebuild every baker, or its .spc files would be refused).
|
||||
SPRITEBAKE_HDRS := $(wildcard $(INCLUDE_DIR)/joey/*.h $(SRC_CORE)/*.h $(SRC_CG)/*.h)
|
||||
|
||||
.PHONY: all tools clean-tools modloopend \
|
||||
.PHONY: all tools clean-tools modloopend tileBankExpect $(COMMON_TOOLS) $(GATE_TOOLS) \
|
||||
spritebake-dos spritebake-amiga spritebake-atarist spritebake-iigs spritebake-x68000
|
||||
all tools: $(MODLOOPEND_BIN)
|
||||
all tools: $(MODLOOPEND_BIN) $(addprefix $(BUILD_DIR)/,$(COMMON_TOOLS))
|
||||
modloopend: $(MODLOOPEND_BIN)
|
||||
tileBankExpect: $(BUILD_DIR)/tileBankExpect
|
||||
$(foreach t,$(COMMON_TOOLS),$(eval $(t): $(BUILD_DIR)/$(t)))
|
||||
$(foreach t,$(GATE_TOOLS),$(eval $(t): $(BUILD_DIR)/$(t)))
|
||||
spritebake-dos: $(BUILD_DIR)/spritebake-dos
|
||||
spritebake-amiga: $(BUILD_DIR)/spritebake-amiga
|
||||
spritebake-atarist: $(BUILD_DIR)/spritebake-atarist
|
||||
spritebake-iigs: $(BUILD_DIR)/spritebake-iigs
|
||||
spritebake-x68000: $(BUILD_DIR)/spritebake-x68000
|
||||
|
||||
# $(call commonToolRule,NAME,SOURCE): NAME from SOURCE and tools/common.
|
||||
define commonToolRule
|
||||
$(BUILD_DIR)/$(1): $(2) $(wildcard $(dir $(2))*.h) $(TOOL_COMMON_SRC) $(TOOL_COMMON_HDRS)
|
||||
@mkdir -p $$(dir $$@)
|
||||
$(HOST_CC) $(HOST_CFLAGS) -I$(TOOLS_DIR)/common $$(filter %.c,$$^) -o $$@ -lm
|
||||
endef
|
||||
$(foreach t,$(COMMON_TOOLS),$(eval $(call commonToolRule,$(t),$(TOOLS_DIR)/$(t)/$(t).c)))
|
||||
$(eval $(call commonToolRule,tileBankExpect,$(REPO_DIR)/tests/host/tileBankExpect.c))
|
||||
|
||||
# $(call gateToolRule,NAME): tests/gate/NAME.c over tools/common and
|
||||
# tests/gate/common.
|
||||
define gateToolRule
|
||||
$(BUILD_DIR)/$(1): $(GATE_DIR)/$(1).c $(GATE_COMMON_SRC) $(GATE_COMMON_HDRS) $(TOOL_COMMON_SRC) $(TOOL_COMMON_HDRS)
|
||||
@mkdir -p $$(dir $$@)
|
||||
$(HOST_CC) $(HOST_CFLAGS) -I$(TOOLS_DIR)/common -I$(GATE_DIR)/common $$(filter %.c,$$^) -o $$@ -lm
|
||||
endef
|
||||
$(foreach t,$(GATE_TOOLS),$(eval $(call gateToolRule,$(t))))
|
||||
|
||||
$(MODLOOPEND_BIN): $(MODLOOPEND_SRC) $(SRC_CORE)/modStopMarker.h
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) $(HOST_CFLAGS) -I$(SRC_CORE) $(MODLOOPEND_SRC) -o $@
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
# the real Kickstart on a private Xvfb display, polls the staged
|
||||
# joeylog.txt until UBER prints its "press any key to exit" completion
|
||||
# marker (or a wall-clock timeout), then kills the emulator and copies
|
||||
# the log to build/amiga/bin/joeylog.txt for tools/uber-perf-table.
|
||||
# the log to build/amiga/bin/joeylog.txt for build/tools/uberPerfTable.
|
||||
#
|
||||
# scripts/bench-amiga.sh # default 150s timeout
|
||||
# TIMEOUT=240 scripts/bench-amiga.sh # override
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
# UBER writes "UBER: <op>: N iters / M frames = K ops/sec" lines to
|
||||
# joeylog.txt in its CWD == the GEMDOS drive C: == the bin dir, so the
|
||||
# log lands directly in build/<build>/bin/joeylog.txt (for the ST build
|
||||
# where tools/uber-perf-table expects it). A copy is kept as
|
||||
# where build/tools/uberPerfTable expects it). A copy is kept as
|
||||
# joeylog-MACHINE.txt beside it, so runs on several machines can be
|
||||
# compared afterwards.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
#
|
||||
# The stage is the generic chunky one, so the hashes must equal the DOS
|
||||
# port's:
|
||||
# scripts/bench-be.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/haiku/bin/joeylog.txt
|
||||
# scripts/bench-be.sh haiku32 && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/haiku32/bin/joeylog.txt
|
||||
# scripts/bench-be.sh beos && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/beos/bin/joeylog.txt
|
||||
# scripts/bench-be.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/haiku/bin/joeylog.txt
|
||||
# scripts/bench-be.sh haiku32 && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/haiku32/bin/joeylog.txt
|
||||
# scripts/bench-be.sh beos && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/beos/bin/joeylog.txt
|
||||
# The ops/sec are this machine's in a VM: compare them only run against run.
|
||||
#
|
||||
# scripts/bench-be.sh [haiku|haiku32|beos] # default haiku (x86_64)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# Mounts build/dos/bin as C: in a headless DOSBox (Xvfb), runs UBER.EXE
|
||||
# (which writes JOEYLOG.TXT to its CWD == C: == the host bin dir), polls
|
||||
# that log for UBER's "press any key to exit" completion marker, then
|
||||
# kills DOSBox. tools/uber-perf-table reads build/dos/bin/JOEYLOG.TXT.
|
||||
# kills DOSBox. build/tools/uberPerfTable reads build/dos/bin/JOEYLOG.TXT.
|
||||
#
|
||||
# Uses the same dosbox-386sx16.conf as joey-run so the cycle budget
|
||||
# matches the claudeDocs/PERF.md reference machine.
|
||||
|
|
@ -74,7 +74,7 @@ if [[ ! -f $log ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# uber-perf-table reads build/dos/bin/JOEYLOG.TXT directly (uppercase).
|
||||
# uberPerfTable reads build/dos/bin/JOEYLOG.TXT directly (uppercase).
|
||||
ops=$(grep -c "ops/sec" "$log" || true)
|
||||
done_marker=$(grep -c "press any key to exit" "$log" || true)
|
||||
if (( done_marker == 0 )); then
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@
|
|||
# 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\<VENDOR>\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.
|
||||
# build/dvx/bin/joeylog.txt (build/tools/diffUberHashes and
|
||||
# build/tools/uberPerfTable 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
|
||||
# scripts/bench-dvx.sh && build/tools/diffUberHashes 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.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
# polls the Super Hi-Res framebuffer for UBER's solid-green "done" screen
|
||||
# (jlSurfaceClear color 2 -> SHR bytes 0x22). When seen, MAME exits and
|
||||
# cadius extracts JOEYLOG.TXT from the work image into build/iigs/bin/
|
||||
# joeylog.txt, where tools/uber-perf-table and diff-uber-hashes expect it.
|
||||
# joeylog.txt, where build/tools/uberPerfTable and diffUberHashes expect it.
|
||||
#
|
||||
# Usage: scripts/bench-iigs.sh
|
||||
# MAME_ROMPATH override ROM dir (default ~/.mame/roms)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# The stage is the generic chunky one, so the hashes must equal the DOS
|
||||
# port's:
|
||||
# scripts/bench-psp.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/psp/bin/UBER/joeylog.txt
|
||||
# scripts/bench-psp.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/psp/bin/UBER/joeylog.txt
|
||||
# The ops/sec are PPSSPP's emulated clock, which runs unthrottled: compare
|
||||
# them only run against run.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# The stage is the generic chunky one, so the hashes must equal the DOS
|
||||
# port's:
|
||||
# scripts/bench-sdl3.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
|
||||
# scripts/bench-sdl3.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
|
||||
# The ops/sec are this machine's, not a reference machine's: compare them
|
||||
# only run against run.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# The stage is the generic chunky one, so the hashes must equal the DOS
|
||||
# port's:
|
||||
# scripts/bench-web.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
|
||||
# scripts/bench-web.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
|
||||
# The ops/sec are this machine's browser's: compare them only run against run.
|
||||
#
|
||||
# scripts/bench-web.sh # default 300 s timeout
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#
|
||||
# The stage is the generic chunky one, so the hashes must equal the DOS
|
||||
# port's:
|
||||
# scripts/bench-win16.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/win16/bin/JOEYLOG.TXT
|
||||
# scripts/bench-win16.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/win16/bin/JOEYLOG.TXT
|
||||
# The ops/sec are this machine's with DOSBox at full speed: compare them
|
||||
# only run against run.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# The stage is the generic chunky one, so the hashes must equal the DOS
|
||||
# port's:
|
||||
# scripts/bench-win32.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/win32/bin/joeylog.txt
|
||||
# scripts/bench-win32.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/win32/bin/joeylog.txt
|
||||
# The ops/sec are this machine's under Wine: compare them only run against run.
|
||||
#
|
||||
# scripts/bench-win32.sh # default 300 s timeout
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
# folder beside it.
|
||||
#
|
||||
# The HTML build fails if any code it shows holds a character that is not
|
||||
# ASCII (scripts/lib/checkDocsCode.py): Asciidoctor turns `->` into an arrow
|
||||
# ASCII (tests/gate/checkDocsCode.c): Asciidoctor turns `->` into an arrow
|
||||
# and `...` into an ellipsis even in a code span, so write those as `+text+`.
|
||||
#
|
||||
# Usage: scripts/build-docs.sh [html|pdf]
|
||||
|
|
@ -25,6 +25,8 @@
|
|||
set -euo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
# shellcheck source=scripts/lib/joeyEnv.sh
|
||||
source "$repo/scripts/lib/joeyEnv.sh"
|
||||
src=$repo/docs/Manual.adoc
|
||||
out=$repo/build/docs
|
||||
what=${1:-both}
|
||||
|
|
@ -53,7 +55,7 @@ common=(-a "revdate=$(date +%Y-%m-%d)" -a allow-uri-read)
|
|||
if [ "$what" = "html" ] || [ "$what" = "both" ]; then
|
||||
asciidoctor "${common[@]}" -a data-uri \
|
||||
"$src" -o "$out/JoeyLib-Manual.html"
|
||||
python3 "$repo/scripts/lib/checkDocsCode.py" "$out/JoeyLib-Manual.html"
|
||||
"$(joeyTool checkDocsCode)" "$out/JoeyLib-Manual.html"
|
||||
echo "build-docs: $out/JoeyLib-Manual.html"
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -45,18 +45,23 @@ done
|
|||
|
||||
# Link-completeness: any JoeyLib symbol undefined across the whole object set
|
||||
# (and not defined by any of them) is a real gap in blank.c or the generics.
|
||||
missing=$(python3 - "$work" <<'PY'
|
||||
import subprocess, sys, glob
|
||||
defs=set(); und=set()
|
||||
for o in glob.glob(sys.argv[1]+"/*.o"):
|
||||
for line in subprocess.run(["nm",o],capture_output=True,text=True).stdout.splitlines():
|
||||
p=line.split()
|
||||
if len(p)>=3 and p[1] in "TDBRtdbr": defs.add(p[2])
|
||||
elif len(p)>=3 and p[1]=="U": und.add(p[2])
|
||||
elif len(p)==2 and p[0]=="U": und.add(p[1])
|
||||
print(" ".join(sorted(s for s in und-defs if s.startswith(("jlp","jlSprite","jlSpriteCompiledDraw")))))
|
||||
PY
|
||||
)
|
||||
missing=$(nm "$work"/*.o 2> /dev/null | LC_ALL=C awk '
|
||||
NF >= 3 && index("TDBRtdbr", $2) > 0 { defs[$3] = 1; next }
|
||||
NF >= 3 && $2 == "U" { undefined[$3] = 1; next }
|
||||
NF == 2 && $1 == "U" { undefined[$2] = 1 }
|
||||
END {
|
||||
n = 0
|
||||
for (s in undefined) {
|
||||
if (!(s in defs) && (s ~ /^jlp/ || s ~ /^jlSprite/)) {
|
||||
names[++n] = s
|
||||
}
|
||||
}
|
||||
asort(names)
|
||||
for (i = 1; i <= n; i++) {
|
||||
printf "%s%s", (i > 1 ? " " : ""), names[i]
|
||||
}
|
||||
print ""
|
||||
}')
|
||||
if [ -n "$missing" ]; then
|
||||
echo "check-blank: FAIL - unresolved JoeyLib symbols (blank.c / generics gap):" >&2
|
||||
echo " $missing" >&2
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
#
|
||||
# check-icons.sh - Host gate for programs' icons: on every port built here,
|
||||
# DRAW (examples/draw) carries the icon make baked it (make/common.mk's
|
||||
# programIcon, tools/iconBake.py), where its system looks for it. Read with
|
||||
# the systems' own tools where they are installed, otherwise by the format:
|
||||
# programIcon, tools/iconBake), where its system looks for it. Read with the
|
||||
# systems' own tools where they are installed, otherwise by the format:
|
||||
#
|
||||
# amiga Draw.info beside it: a DiskObject (magic $E310) for a tool,
|
||||
# AMIGA_ICON_STACK bytes of stack, the image iconBake makes
|
||||
|
|
@ -39,6 +39,44 @@ label=check-icons
|
|||
fails=""
|
||||
checked=""
|
||||
|
||||
# The Amiga's Workbench icon (workbench.h): a DiskObject's magic and a tool's
|
||||
# do_Type, at their offsets with do_StackSize's and its Image's Width,
|
||||
# Height, and Depth; iconBake's tool icons are 48x24 in 2 planes.
|
||||
WB_DISKMAGIC=$((0xE310))
|
||||
WBTOOL=3
|
||||
DO_TYPE_AT=48
|
||||
DO_STACK_AT=74
|
||||
IMAGE_SIZE_AT=82
|
||||
AMIGA_ICON="48 24 2"
|
||||
# Win16's NE header (its offset at 0x3C): the resource and resident-name
|
||||
# tables' offsets, equal when there are no resources; a type's entry is 8
|
||||
# bytes, a resource's 12 with its ID 6 bytes in; an integer ID has bit 15.
|
||||
NE_HEADER_AT=$((0x3C))
|
||||
NE_RESOURCES_AT=$((0x24))
|
||||
NE_NAMES_AT=$((0x26))
|
||||
NE_TYPE_BYTES=8
|
||||
NE_RESOURCE_BYTES=12
|
||||
NE_RESOURCE_ID_AT=6
|
||||
RT_GROUP_ICON=$((0x800E))
|
||||
INTEGER_ID=$((0x8000))
|
||||
# A PNG's signature, and the width and height in its IHDR.
|
||||
PNG_SIGNATURE=89504e470d0a1a0a
|
||||
PNG_WIDTH_AT=16
|
||||
PNG_HEIGHT_AT=20
|
||||
# The PSP's EBOOT: "\0PBP", then the parts' offsets from byte 8, ICON0 the
|
||||
# second (and the third's start its end).
|
||||
PBP_MAGIC=00504250
|
||||
PBP_ICON0_AT=12
|
||||
PBP_ICON1_AT=16
|
||||
# The IIgs icon file: its file ID, and in its first record (from byte 26)
|
||||
# the application's name (a Pascal string at 66) and file type (at 82).
|
||||
IIGS_FILE_ID_AT=4
|
||||
IIGS_RECORD_AT=26
|
||||
IIGS_NAME_AT=66
|
||||
IIGS_TYPE_AT=82
|
||||
IIGS_ICON_FILE_ID=1
|
||||
IIGS_APPLICATION=$((0xB3))
|
||||
|
||||
fail() {
|
||||
fails="${fails:+$fails; }$1: $2"
|
||||
}
|
||||
|
|
@ -48,31 +86,60 @@ makeValue() {
|
|||
make -s -f "$repo/make/$1.mk" --eval "valueOf: ; @echo \$($2)" valueOf 2> /dev/null
|
||||
}
|
||||
|
||||
# fileNumber FILE OFFSET BYTES be|le: an unsigned number in FILE; fails past
|
||||
# its end.
|
||||
fileNumber() {
|
||||
od -An -tu1 -v -j "$2" -N "$3" "$1" 2> /dev/null | awk -v n="$3" -v endian="$4" '
|
||||
{ for (i = 1; i <= NF; i++) { b[k++] = $i } }
|
||||
END { if (k < n) { exit 1 } v = 0; for (i = 0; i < n; i++) { v = v * 256 + b[endian == "be" ? i : n - 1 - i] } print v }'
|
||||
}
|
||||
|
||||
# fileHex FILE OFFSET BYTES: those bytes as lower-case hex.
|
||||
fileHex() {
|
||||
od -An -tx1 -v -j "$2" -N "$3" "$1" 2> /dev/null | tr -d ' \n'
|
||||
}
|
||||
|
||||
# neGroupIcon EXE ID: true when the Win16 EXE's NE resource table holds a
|
||||
# group icon numbered ID.
|
||||
neGroupIcon() {
|
||||
local exe=$1
|
||||
local want=$((INTEGER_ID | $2))
|
||||
local ne
|
||||
local table
|
||||
local at
|
||||
local typeId
|
||||
local count
|
||||
local i
|
||||
|
||||
ne=$(fileNumber "$exe" "$NE_HEADER_AT" 4 le) || return 1
|
||||
table=$((ne + $(fileNumber "$exe" $((ne + NE_RESOURCES_AT)) 2 le)))
|
||||
[ "$table" -ne $((ne + $(fileNumber "$exe" $((ne + NE_NAMES_AT)) 2 le))) ] || return 1
|
||||
at=$((table + 2))
|
||||
while true; do
|
||||
typeId=$(fileNumber "$exe" "$at" 2 le) || return 1
|
||||
count=$(fileNumber "$exe" $((at + 2)) 2 le) || return 1
|
||||
[ "$typeId" -ne 0 ] || return 1
|
||||
at=$((at + NE_TYPE_BYTES))
|
||||
for ((i = 0; i < count; i++)); do
|
||||
if [ "$typeId" -eq "$RT_GROUP_ICON" ] && [ "$(fileNumber "$exe" $((at + NE_RESOURCE_ID_AT)) 2 le)" = "$want" ]; then
|
||||
return 0
|
||||
fi
|
||||
at=$((at + NE_RESOURCE_BYTES))
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
for port in amiga win32 win16 sdl3 macos web psp haiku haiku32 beos dvx iigs; do
|
||||
program=$(joeyExampleProgram draw "$port" 2> /dev/null) || continue
|
||||
[ -f "$program" ] || continue
|
||||
checked="${checked:+$checked }$port"
|
||||
case "$port" in
|
||||
amiga)
|
||||
python3 - "$program.info" "$(makeValue amiga AMIGA_ICON_STACK)" "$repo/tools" <<'PY' || fail amiga "Draw.info"
|
||||
import struct
|
||||
import sys
|
||||
|
||||
info, stack, tools = sys.argv[1], int(sys.argv[2]), sys.argv[3]
|
||||
sys.path.insert(0, tools)
|
||||
import iconBake # noqa: E402
|
||||
|
||||
data = open(info, "rb").read()
|
||||
magic, version = struct.unpack_from(">HH", data, 0)
|
||||
doType = data[48]
|
||||
doStack = struct.unpack_from(">I", data, 74)[0]
|
||||
w, h, depth = struct.unpack_from(">hhh", data, 82)
|
||||
want = (iconBake.WB_DISKMAGIC, iconBake.WBTOOL, stack, iconBake.AMIGA_W, iconBake.AMIGA_H, iconBake.AMIGA_DEPTH)
|
||||
got = (magic, doType, doStack, w, h, depth)
|
||||
if got != want:
|
||||
print(" amiga: magic, type, stack, image %s, want %s" % (got, want))
|
||||
sys.exit(1)
|
||||
PY
|
||||
info=$program.info
|
||||
want="$WB_DISKMAGIC $WBTOOL $(makeValue amiga AMIGA_ICON_STACK) $AMIGA_ICON"
|
||||
got="$(fileNumber "$info" 0 2 be) $(fileNumber "$info" "$DO_TYPE_AT" 1 be) $(fileNumber "$info" "$DO_STACK_AT" 4 be)"
|
||||
got="$got $(fileNumber "$info" "$IMAGE_SIZE_AT" 2 be) $(fileNumber "$info" $((IMAGE_SIZE_AT + 2)) 2 be) $(fileNumber "$info" $((IMAGE_SIZE_AT + 4)) 2 be)"
|
||||
[ "$got" = "$want" ] || fail amiga "Draw.info: magic, type, stack, image $got, want $want"
|
||||
;;
|
||||
win32)
|
||||
readobj=$(dirname "$WIN32_CC")/llvm-readobj
|
||||
|
|
@ -81,72 +148,35 @@ PY
|
|||
awk -v want="Name: (ID $id)" '/Type: GROUP_ICON/ { group = 1; next } group && /Name:/ { found = index($0, want) > 0; exit } END { exit !found }' <<< "$listing" || fail win32 "no group icon $id among DRAW.EXE's resources"
|
||||
;;
|
||||
win16)
|
||||
python3 - "$program" "$(makeValue win16 HOSTED_ICON_ID)" <<'PY' || fail win16 "no group icon in DRAW.EXE's NE resources"
|
||||
import struct
|
||||
import sys
|
||||
|
||||
RT_GROUP_ICON = 0x800E
|
||||
INTEGER_ID = 0x8000
|
||||
data = open(sys.argv[1], "rb").read()
|
||||
wantId = INTEGER_ID | int(sys.argv[2])
|
||||
ne = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
table = ne + struct.unpack_from("<H", data, ne + 0x24)[0]
|
||||
if table == ne + struct.unpack_from("<H", data, ne + 0x26)[0]:
|
||||
sys.exit(1)
|
||||
at = table + 2
|
||||
while True:
|
||||
typeId, count = struct.unpack_from("<HH", data, at)
|
||||
if typeId == 0:
|
||||
sys.exit(1)
|
||||
at += 8
|
||||
for i in range(count):
|
||||
if typeId == RT_GROUP_ICON and struct.unpack_from("<H", data, at + 6)[0] == wantId:
|
||||
sys.exit(0)
|
||||
at += 12
|
||||
PY
|
||||
neGroupIcon "$program" "$(makeValue win16 HOSTED_ICON_ID)" || fail win16 "no group icon in DRAW.EXE's NE resources"
|
||||
;;
|
||||
sdl3)
|
||||
symbols=$(nm "$program" 2> /dev/null)
|
||||
grep -q ' jlgHostedIconRgba$' <<< "$symbols" || fail sdl3 "DRAW links no window icon"
|
||||
;;
|
||||
macos)
|
||||
python3 - "$(dirname "$(dirname "$(dirname "$program")")")" <<'PY' || fail macos "DRAW.app's icon"
|
||||
import plistlib
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
app = sys.argv[1]
|
||||
with open(app + "/Contents/Info.plist", "rb") as f:
|
||||
info = plistlib.load(f)
|
||||
icon = Image.open("%s/Contents/Resources/%s.icns" % (app, info["CFBundleIconFile"]))
|
||||
sys.exit(icon.format != "ICNS")
|
||||
PY
|
||||
app=$(dirname "$(dirname "$(dirname "$program")")")
|
||||
iconName=$(awk '/<key>CFBundleIconFile<\/key>/ { getline; gsub(/.*<string>|<\/string>.*/, ""); print; exit }' "$app/Contents/Info.plist" 2> /dev/null)
|
||||
icns=$app/Contents/Resources/$iconName.icns
|
||||
if [ -z "$iconName" ] || [ "$(head -c 4 "$icns" 2> /dev/null)" != icns ] || [ "$(fileNumber "$icns" 4 4 be)" != "$(wc -c < "$icns" | tr -d " ")" ]; then
|
||||
fail macos "DRAW.app's icon"
|
||||
fi
|
||||
;;
|
||||
web)
|
||||
folder=$(dirname "$program")
|
||||
grep -qE '<link[^>]*rel=("?)icon\1[^>]*>' "$program" && grep -qE 'href=("?)icon\.png' "$program" || fail web "DRAW.html names no icon.png"
|
||||
python3 -c 'import sys; from PIL import Image; sys.exit(Image.open(sys.argv[1]).format != "PNG")' "$folder/icon.png" 2> /dev/null || fail web "no icon.png beside DRAW.html"
|
||||
[ "$(fileHex "$folder/icon.png" 0 8)" = "$PNG_SIGNATURE" ] || fail web "no icon.png beside DRAW.html"
|
||||
;;
|
||||
psp)
|
||||
python3 - "$program" "$repo/build/psp/icon/DRAW/icon0.png" "$(makeValue psp PSP_ICON0_SIZE)" <<'PY' || fail psp "the EBOOT's ICON0"
|
||||
import io
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
eboot, baked, size = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
data = open(eboot, "rb").read()
|
||||
if data[:4] != b"\0PBP":
|
||||
sys.exit(1)
|
||||
offsets = struct.unpack_from("<8I", data, 8)
|
||||
icon = data[offsets[1]:offsets[2]]
|
||||
picture = Image.open(io.BytesIO(icon))
|
||||
if icon != open(baked, "rb").read() or "%dx%d" % picture.size != size:
|
||||
print(" psp: ICON0 is %d bytes, %dx%d" % (len(icon), picture.size[0], picture.size[1]))
|
||||
sys.exit(1)
|
||||
PY
|
||||
baked=$repo/build/psp/icon/DRAW/icon0.png
|
||||
size=$(makeValue psp PSP_ICON0_SIZE)
|
||||
start=$(fileNumber "$program" "$PBP_ICON0_AT" 4 le)
|
||||
end=$(fileNumber "$program" "$PBP_ICON1_AT" 4 le)
|
||||
if [ "$(fileHex "$program" 0 4)" != "$PBP_MAGIC" ] || ! cmp -s <(tail -c +$((start + 1)) "$program" | head -c $((end - start))) "$baked"; then
|
||||
fail psp "the EBOOT's ICON0 is not the baked icon"
|
||||
elif [ "$(fileNumber "$baked" "$PNG_WIDTH_AT" 4 be)x$(fileNumber "$baked" "$PNG_HEIGHT_AT" 4 be)" != "$size" ]; then
|
||||
fail psp "the EBOOT's ICON0 is not $size"
|
||||
fi
|
||||
;;
|
||||
haiku|haiku32|beos)
|
||||
listing=$("$HAIKU_TOOLS/xres" -l "$program" 2> /dev/null)
|
||||
|
|
@ -169,21 +199,9 @@ PY
|
|||
iconFile=$(find "$out" -iname 'DRAW.ICONS*' -not -name '_FileInformation.txt' -print -quit)
|
||||
listing=$("$CADIUS" CATALOG "$disk" 2> /dev/null)
|
||||
grep -qE '^ +DRAW\.ICONS +ICN ' <<< "$listing" || fail iigs "no DRAW.ICONS of type \$CA in joey.2mg's Icons folder"
|
||||
python3 - "$iconFile" <<'PY' || fail iigs "DRAW.ICONS is no icon file for DRAW"
|
||||
import struct
|
||||
import sys
|
||||
|
||||
ICON_FILE_ID = 0x0001
|
||||
APPLICATION = 0x00B3
|
||||
data = open(sys.argv[1], "rb").read()
|
||||
fileId = struct.unpack_from("<H", data, 4)[0]
|
||||
record = 26
|
||||
name = data[record + 67:record + 67 + data[record + 66]].decode("ascii")
|
||||
fileType = struct.unpack_from("<H", data, record + 82)[0]
|
||||
if (fileId, name, fileType) != (ICON_FILE_ID, "DRAW", APPLICATION):
|
||||
print(" iigs: id, name, type %s" % ((fileId, name, fileType),))
|
||||
sys.exit(1)
|
||||
PY
|
||||
nameLength=$(fileNumber "$iconFile" $((IIGS_RECORD_AT + IIGS_NAME_AT)) 1 le)
|
||||
got="$(fileNumber "$iconFile" "$IIGS_FILE_ID_AT" 2 le) $(dd if="$iconFile" bs=1 skip=$((IIGS_RECORD_AT + IIGS_NAME_AT + 1)) count="${nameLength:-0}" status=none 2> /dev/null) $(fileNumber "$iconFile" $((IIGS_RECORD_AT + IIGS_TYPE_AT)) 2 le)"
|
||||
[ "$got" = "$IIGS_ICON_FILE_ID DRAW $IIGS_APPLICATION" ] || fail iigs "DRAW.ICONS is no icon file for DRAW (id, name, type $got)"
|
||||
rm -rf "$out"
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# check-macos-apps.sh - Host gate for the macOS port's apps (make macos),
|
||||
# which nothing here can run: every app in build/macos/bin is one macOS can
|
||||
# open. Its Info.plist parses and names, as an application for the macOS
|
||||
# open. Its Info.plist names, as an application for the macOS
|
||||
# make/support/macos/cc.sh builds for, a program that is in Contents/MacOS
|
||||
# and an icon that is in Contents/Resources. The program is universal: an
|
||||
# Apple silicon (arm64) and an Intel (x86_64) program, each built for that
|
||||
|
|
@ -31,68 +31,92 @@ if ! compgen -G "$bin/*.app" > /dev/null; then
|
|||
exit 2
|
||||
fi
|
||||
|
||||
python3 - "$bin" "$MACOS_LIPO" "$objdump" "$minimum" "$label" <<'PY'
|
||||
import glob
|
||||
import os
|
||||
import plistlib
|
||||
import subprocess
|
||||
import sys
|
||||
archs="arm64 x86_64"
|
||||
signed="arm64"
|
||||
|
||||
bin, lipo, objdump, minimum, label = sys.argv[1:]
|
||||
ARCHS = {"arm64", "x86_64"}
|
||||
SIGNED = {"arm64"}
|
||||
SYSTEM = ("/System/Library/", "/usr/lib/")
|
||||
# joinBy SEPARATOR WORD... -- the words with SEPARATOR between them.
|
||||
joinBy() {
|
||||
local separator="$1"
|
||||
local out="$2"
|
||||
shift 2
|
||||
for word in "$@"; do
|
||||
out+="$separator$word"
|
||||
done
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
|
||||
def run(*args):
|
||||
return subprocess.run(args, capture_output=True, text=True, check=True).stdout
|
||||
# plistString PLIST KEY -- the string KEY holds in an Info.plist (the
|
||||
# build's own XML, each value on the line after its key).
|
||||
plistString() {
|
||||
awk -v key="<key>$2</key>" '
|
||||
found {
|
||||
if (match($0, /<string>[^<]*<\/string>/)) {
|
||||
print substr($0, RSTART + length("<string>"), RLENGTH - length("<string></string>"))
|
||||
}
|
||||
exit
|
||||
}
|
||||
index($0, key) {
|
||||
found = 1
|
||||
}' "$1"
|
||||
}
|
||||
|
||||
|
||||
def sliceProblems(program, arch):
|
||||
problems = []
|
||||
headers = run(objdump, "--macho", "--private-headers", "--arch=" + arch, program).split()
|
||||
if "LC_BUILD_VERSION" not in headers or headers[headers.index("minos") + 1] != minimum:
|
||||
problems.append("%s not built for macOS %s" % (arch, minimum))
|
||||
if arch in SIGNED and "LC_CODE_SIGNATURE" not in headers:
|
||||
problems.append("%s unsigned" % arch)
|
||||
for line in run(objdump, "--macho", "--dylibs-used", "--arch=" + arch, program).splitlines()[1:]:
|
||||
library = line.split()[0]
|
||||
if not library.startswith(SYSTEM):
|
||||
problems.append("%s links %s" % (arch, library))
|
||||
return problems
|
||||
# sliceProblems PROGRAM ARCH -- what is wrong with one arch of PROGRAM.
|
||||
sliceProblems() {
|
||||
local headers
|
||||
local library
|
||||
headers=" $("$objdump" --macho --private-headers --arch="$2" "$1" | tr -s ' \t\n' ' ') "
|
||||
if [[ $headers != *" LC_BUILD_VERSION "* || $headers != *" minos $minimum "* ]]; then
|
||||
problems+=("$2 not built for macOS $minimum")
|
||||
fi
|
||||
if [[ " $signed " == *" $2 "* && $headers != *" LC_CODE_SIGNATURE "* ]]; then
|
||||
problems+=("$2 unsigned")
|
||||
fi
|
||||
while read -r library _; do
|
||||
[[ $library == /System/Library/* || $library == /usr/lib/* ]] || problems+=("$2 links $library")
|
||||
done < <("$objdump" --macho --dylibs-used --arch="$2" "$1" | tail -n +2)
|
||||
}
|
||||
|
||||
|
||||
def appProblems(app):
|
||||
contents = os.path.join(app, "Contents")
|
||||
with open(os.path.join(contents, "Info.plist"), "rb") as f:
|
||||
info = plistlib.load(f)
|
||||
problems = []
|
||||
if info.get("CFBundlePackageType") != "APPL" or info.get("LSMinimumSystemVersion") != minimum:
|
||||
problems.append("Info.plist is not an application for macOS %s" % minimum)
|
||||
if not os.path.isfile(os.path.join(contents, "Resources", info.get("CFBundleIconFile", "") + ".icns")):
|
||||
problems.append("no icon")
|
||||
program = os.path.join(contents, "MacOS", info.get("CFBundleExecutable", ""))
|
||||
if not (os.path.isfile(program) and os.access(program, os.X_OK)):
|
||||
return problems + ["no program"]
|
||||
archs = set(run(lipo, "-archs", program).split())
|
||||
if archs != ARCHS:
|
||||
problems.append("holds %s, not %s" % (" ".join(sorted(archs)), " ".join(sorted(ARCHS))))
|
||||
for arch in sorted(archs & ARCHS):
|
||||
problems += sliceProblems(program, arch)
|
||||
return problems
|
||||
# appProblems APP -- what is wrong with APP, in problems.
|
||||
appProblems() {
|
||||
local contents="$1/Contents"
|
||||
local plist="$contents/Info.plist"
|
||||
local program
|
||||
local held
|
||||
local arch
|
||||
problems=()
|
||||
[ -f "$plist" ] || { problems=("no Info.plist"); return; }
|
||||
if [ "$(plistString "$plist" CFBundlePackageType)" != APPL ] || [ "$(plistString "$plist" LSMinimumSystemVersion)" != "$minimum" ]; then
|
||||
problems+=("Info.plist is not an application for macOS $minimum")
|
||||
fi
|
||||
[ -f "$contents/Resources/$(plistString "$plist" CFBundleIconFile).icns" ] || problems+=("no icon")
|
||||
program="$contents/MacOS/$(plistString "$plist" CFBundleExecutable)"
|
||||
if [ ! -f "$program" ] || [ ! -x "$program" ]; then
|
||||
problems+=("no program")
|
||||
return
|
||||
fi
|
||||
# shellcheck disable=SC2046
|
||||
held=$(printf '%s\n' $("$MACOS_LIPO" -archs "$program") | sort | paste -sd' ')
|
||||
[ "$held" = "$archs" ] || problems+=("holds $held, not $archs")
|
||||
for arch in $held; do
|
||||
[[ " $archs " == *" $arch "* ]] && sliceProblems "$program" "$arch"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
apps = sorted(glob.glob(os.path.join(bin, "*.app")))
|
||||
fails = []
|
||||
for app in apps:
|
||||
try:
|
||||
problems = appProblems(app)
|
||||
except (OSError, ValueError, KeyError, plistlib.InvalidFileException, subprocess.CalledProcessError) as error:
|
||||
problems = [str(error)]
|
||||
if problems:
|
||||
fails.append("%s: %s" % (os.path.basename(app), ", ".join(problems)))
|
||||
if fails:
|
||||
print("%s: FAIL - %s" % (label, "; ".join(fails)))
|
||||
sys.exit(1)
|
||||
print("%s: PASS (%d app%s: universal %s, arm64 signed, macOS %s on, system libraries only)" % (label, len(apps), "" if len(apps) == 1 else "s", "+".join(sorted(ARCHS)), minimum))
|
||||
PY
|
||||
fails=()
|
||||
count=0
|
||||
for app in "$bin"/*.app; do
|
||||
count=$((count + 1))
|
||||
appProblems "$app"
|
||||
if [ ${#problems[@]} -gt 0 ]; then
|
||||
fails+=("$(basename "$app"): $(joinBy ', ' "${problems[@]}")")
|
||||
fi
|
||||
done
|
||||
if [ ${#fails[@]} -gt 0 ]; then
|
||||
echo "$label: FAIL - $(joinBy '; ' "${fails[@]}")"
|
||||
exit 1
|
||||
fi
|
||||
echo "$label: PASS ($count app$([ "$count" = 1 ] || echo s): universal ${archs// /+}, arm64 signed, macOS $minimum on, system libraries only)"
|
||||
|
|
|
|||
|
|
@ -37,22 +37,39 @@ fi
|
|||
tables=()
|
||||
asm="${AMIGA_PTPLAYER:-}/ptplayer.asm"
|
||||
if [ -f "$asm" ]; then
|
||||
python3 - "$asm" "$work/tables.txt" <<'EOF'
|
||||
import re, sys
|
||||
asm = open(sys.argv[1]).read()
|
||||
def values(start, stop):
|
||||
block = asm[asm.index(start):asm.index(stop, asm.index(start))]
|
||||
out = []
|
||||
for line in block.splitlines():
|
||||
m = re.match(r'\s*dc\.[bw]\s+([^;]*)', line)
|
||||
if m:
|
||||
out += [int(v) for v in m.group(1).split(',')]
|
||||
return out
|
||||
periods = values('mt_PeriodTable:', '\n\n')
|
||||
waves = values('mt_VibratoSineTable:', 'mt_VibratoSawTable:') + values('mt_VibratoSawTable:', 'mt_VibratoRectTable:') + values('mt_VibratoRectTable:', 'endc')
|
||||
with open(sys.argv[2], 'w') as f:
|
||||
f.write('periods ' + ' '.join(map(str, periods)) + '\nvibrato ' + ' '.join(map(str, waves)) + '\n')
|
||||
EOF
|
||||
awk -v out="$work/tables.txt" '
|
||||
index($0, "mt_PeriodTable:") && !periodsDone {
|
||||
inPeriods = 1
|
||||
}
|
||||
inPeriods && $0 == "" {
|
||||
inPeriods = 0
|
||||
periodsDone = 1
|
||||
}
|
||||
index($0, "mt_VibratoSineTable:") && !wavesDone {
|
||||
inWaves = 1
|
||||
}
|
||||
index($0, "mt_VibratoRectTable:") {
|
||||
afterRect = 1
|
||||
}
|
||||
inWaves && afterRect && index($0, "endc") {
|
||||
inWaves = 0
|
||||
wavesDone = 1
|
||||
}
|
||||
(inPeriods || inWaves) && match($0, /^[ \t]*dc\.[bw][ \t]+/) {
|
||||
list = substr($0, RLENGTH + 1)
|
||||
sub(/;.*/, "", list)
|
||||
n = split(list, values, ",")
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (inPeriods) {
|
||||
periods = periods (periods == "" ? "" : " ") (values[i] + 0)
|
||||
} else {
|
||||
waves = waves (waves == "" ? "" : " ") (values[i] + 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
END {
|
||||
printf "periods %s\nvibrato %s\n", periods, waves > out
|
||||
}' "$asm"
|
||||
tables=("$work/tables.txt")
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -62,68 +62,87 @@ else
|
|||
echo "check-sfxmix: comparing against baseline (exact hash mode)"
|
||||
fi
|
||||
|
||||
if python3 - "$baseline" "$work/out1.txt" "$tolerance" <<'PY'
|
||||
import sys
|
||||
|
||||
|
||||
def parse(path):
|
||||
order = []
|
||||
scen = {}
|
||||
name = None
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("name="):
|
||||
fields = dict(p.split("=", 1) for p in line.split())
|
||||
if awk -v tolerance="$tolerance" '
|
||||
BEGIN {
|
||||
for (i = 0; i < 256; i++) {
|
||||
byteOf[sprintf("%02X", i)] = i
|
||||
}
|
||||
}
|
||||
FNR == 1 {
|
||||
name = ""
|
||||
}
|
||||
{
|
||||
gsub(/^[ \t\r]+|[ \t\r]+$/, "")
|
||||
}
|
||||
/^name=/ {
|
||||
delete fields
|
||||
for (i = 1; i <= NF; i++) {
|
||||
at = index($i, "=")
|
||||
fields[substr($i, 1, at - 1)] = substr($i, at + 1)
|
||||
}
|
||||
name = fields["name"]
|
||||
order.append(name)
|
||||
scen[name] = {"hash": fields["hash"], "slots": int(fields["slots"]), "raw": b""}
|
||||
elif line.startswith("raw=") and name is not None:
|
||||
scen[name]["raw"] = bytes.fromhex(line[4:])
|
||||
return order, scen
|
||||
|
||||
|
||||
baseOrder, base = parse(sys.argv[1])
|
||||
curOrder, cur = parse(sys.argv[2])
|
||||
tolerance = int(sys.argv[3]) if sys.argv[3] else None
|
||||
|
||||
fail = False
|
||||
if curOrder != baseOrder:
|
||||
print("check-sfxmix: scenario list changed vs baseline")
|
||||
print(" baseline: " + " ".join(baseOrder))
|
||||
print(" current: " + " ".join(curOrder))
|
||||
fail = True
|
||||
|
||||
for name in baseOrder:
|
||||
if name not in cur:
|
||||
order[ARGIND, ++count[ARGIND]] = name
|
||||
hash[ARGIND, name] = fields["hash"]
|
||||
slots[ARGIND, name] = fields["slots"] + 0
|
||||
raw[ARGIND, name] = ""
|
||||
seen[ARGIND, name] = 1
|
||||
}
|
||||
/^raw=/ && name != "" {
|
||||
raw[ARGIND, name] = toupper(substr($0, 5))
|
||||
}
|
||||
END {
|
||||
same = count[1] == count[2]
|
||||
for (i = 1; i <= count[1] && same; i++) {
|
||||
same = order[1, i] == order[2, i]
|
||||
}
|
||||
if (!same) {
|
||||
print "check-sfxmix: scenario list changed vs baseline"
|
||||
line = " baseline:"
|
||||
for (i = 1; i <= count[1]; i++) {
|
||||
line = line " " order[1, i]
|
||||
}
|
||||
print (count[1] > 0 ? line : line " ")
|
||||
line = " current: "
|
||||
for (i = 1; i <= count[2]; i++) {
|
||||
line = line " " order[2, i]
|
||||
}
|
||||
print (count[2] > 0 ? line : line " ")
|
||||
failed = 1
|
||||
}
|
||||
for (i = 1; i <= count[1]; i++) {
|
||||
name = order[1, i]
|
||||
if (!((2, name) in seen)) {
|
||||
continue
|
||||
b = base[name]
|
||||
c = cur[name]
|
||||
label = " %-24s" % (name + ":")
|
||||
if len(b["raw"]) != len(c["raw"]):
|
||||
print("%s FAIL (length %d -> %d)" % (label, len(b["raw"]), len(c["raw"])))
|
||||
fail = True
|
||||
}
|
||||
label = sprintf(" %-24s", name ":")
|
||||
baseLen = length(raw[1, name]) / 2
|
||||
curLen = length(raw[2, name]) / 2
|
||||
if (baseLen != curLen) {
|
||||
printf "%s FAIL (length %d -> %d)\n", label, baseLen, curLen
|
||||
failed = 1
|
||||
continue
|
||||
if tolerance is None:
|
||||
ok = (b["hash"] == c["hash"]) and (b["raw"] == c["raw"])
|
||||
if ok:
|
||||
print("%s PASS (hash %s)" % (label, c["hash"]))
|
||||
else:
|
||||
print("%s FAIL (hash %s -> %s)" % (label, b["hash"], c["hash"]))
|
||||
fail = True
|
||||
else:
|
||||
}
|
||||
if (tolerance == "") {
|
||||
if (hash[1, name] == hash[2, name] && raw[1, name] == raw[2, name]) {
|
||||
printf "%s PASS (hash %s)\n", label, hash[2, name]
|
||||
} else {
|
||||
printf "%s FAIL (hash %s -> %s)\n", label, hash[1, name], hash[2, name]
|
||||
failed = 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
maxDelta = 0
|
||||
for x, y in zip(b["raw"], c["raw"]):
|
||||
d = abs(x - y)
|
||||
if d > maxDelta:
|
||||
maxDelta = d
|
||||
allowed = tolerance * b["slots"]
|
||||
ok = maxDelta <= allowed
|
||||
print("%s maxAbsDelta=%d (allowed %d) %s" % (label, maxDelta, allowed, "PASS" if ok else "FAIL"))
|
||||
fail = fail or not ok
|
||||
|
||||
sys.exit(1 if fail else 0)
|
||||
PY
|
||||
for (b = 1; b <= baseLen; b++) {
|
||||
delta = byteOf[substr(raw[1, name], 2 * b - 1, 2)] - byteOf[substr(raw[2, name], 2 * b - 1, 2)]
|
||||
delta = delta < 0 ? -delta : delta
|
||||
maxDelta = delta > maxDelta ? delta : maxDelta
|
||||
}
|
||||
allowed = tolerance * slots[1, name]
|
||||
printf "%s maxAbsDelta=%d (allowed %d) %s\n", label, maxDelta, allowed, (maxDelta <= allowed ? "PASS" : "FAIL")
|
||||
failed = failed || maxDelta > allowed
|
||||
}
|
||||
exit failed ? 1 : 0
|
||||
}' "$baseline" "$work/out1.txt"
|
||||
then
|
||||
echo "check-sfxmix: PASS"
|
||||
else
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# check-tilebank.sh - Host-cc gate for the baked tile-bank loaders
|
||||
# (jlTileBankLoad / jlTileBankLoadToSurface in src/core/assetLoad.c), end to
|
||||
# end: assetbake.py bakes tests/host/tileBank.png, the real loader reads it,
|
||||
# end: assetbake bakes tests/host/tileBank.png, the real loader reads it,
|
||||
# the real generic tile paste draws it, and tests/host/tileBankHost.c checks
|
||||
# every pixel against the PNG. Also checks the refusals (a bank baked for any
|
||||
# other target, bad magic, short header, missing file), a truncated bank,
|
||||
|
|
@ -17,12 +17,13 @@
|
|||
# target byte there, so the DOS bake is the bank that must load. Every other
|
||||
# target assetbake knows is baked too and must be refused.
|
||||
#
|
||||
# tests/host/tileBankStage.py stages the banks and the expected pixels and
|
||||
# palette (decoded from the PNG with PIL, independent of assetbake's
|
||||
# encoders), exactly as make/<port>.mk stages them for examples/tilebank on
|
||||
# the real ports. The harness prints the surface hash of its to-surface load;
|
||||
# scripts/verify-<port>-tilebank.sh run this script and require each port's
|
||||
# own bake to hash the same. Exits nonzero on any failed check.
|
||||
# tests/host/tileBankStage.sh stages the banks and the expected pixels and
|
||||
# palette (decoded from the PNG by tileBankExpect, independent of assetbake's
|
||||
# encoders; make/tools.mk builds both), exactly as make/examples.mk stages
|
||||
# them for examples/tilebank on the real ports. The harness prints the
|
||||
# surface hash of its to-surface load; scripts/verify-<port>-tilebank.sh run
|
||||
# this script and require each port's own bake to hash the same. Exits
|
||||
# nonzero on any failed check.
|
||||
set -euo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
|
|
@ -38,7 +39,8 @@ cd "$repo"
|
|||
src/core/*.c src/generic/*.c src/blank/blank.c tests/host/tileBankHost.c \
|
||||
-o "$work/tileBankHost"
|
||||
|
||||
python3 "$repo/tests/host/tileBankStage.py" "$hostTarget" "$png" "$work/DATA"
|
||||
make -s -f "$repo/make/tools.mk" assetbake tileBankExpect > /dev/null
|
||||
"$repo/tests/host/tileBankStage.sh" "$hostTarget" "$png" "$work/DATA"
|
||||
refuse=()
|
||||
for bank in "$work"/DATA/refuse*.tbk; do
|
||||
refuse+=("$(basename "$bank")")
|
||||
|
|
|
|||
|
|
@ -3,26 +3,31 @@
|
|||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# check-tiled.sh - Host gate for tools/tiled2joey.py, which turns a map
|
||||
# made in Tiled into a tile sheet (baked into tile banks by assetbake.py) and
|
||||
# one jlTileMapPaste map per tile layer (docs/Assets.adoc). tests/host/
|
||||
# tiledCheck.py writes one map as TMX and as TMJ, with every flip Tiled can
|
||||
# ask for, two tilesets (one external, with a margin and spacing), and every
|
||||
# layer encoding the tool reads; both must convert to the sheet and maps
|
||||
# worked out by hand, the sheet must bake, and maps it cannot take (16x16
|
||||
# tiles, an RGB tileset) must be refused.
|
||||
# check-tiled.sh - Host gate for tools/tiled2joey, which turns a map
|
||||
# made in Tiled into a tile sheet (baked into tile banks by assetbake) and
|
||||
# one jlTileMapPaste map per tile layer (docs/Assets.adoc). tiledCheck
|
||||
# (tests/gate/tiledCheck.c) writes one map as TMX and as TMJ, with every flip
|
||||
# Tiled can ask for, two tilesets (one external, with a margin and spacing),
|
||||
# and every layer encoding the tool reads; both must convert to the sheet and
|
||||
# maps worked out by hand, the sheet must bake, and maps it cannot take (16x16
|
||||
# tiles, a tileset that is not indexed) must be refused.
|
||||
#
|
||||
# Usage: scripts/check-tiled.sh
|
||||
# Exit: 0 PASS, 1 FAIL
|
||||
set -uo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
source "$repo/scripts/lib/joeyEnv.sh"
|
||||
label=check-tiled
|
||||
|
||||
work=$(mktemp -d -t joeylib-tiled.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
out=$(python3 "$repo/tests/host/tiledCheck.py" "$repo" "$work")
|
||||
# The converter, and the assetbake the sheet is baked with: make/tools.mk's,
|
||||
# which tiledCheck runs from JOEY_BUILD's tools/.
|
||||
make -s -f "$repo/make/tools.mk" tiled2joey assetbake > /dev/null || exit 1
|
||||
tiledCheck=$(joeyTool tiledCheck) || exit 1
|
||||
out=$("$tiledCheck" "$repo" "$work")
|
||||
status=$?
|
||||
echo "$out"
|
||||
checks=$(grep -c '^TILED:' <<< "$out")
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# check-vgm.sh - Host gate for tools/vgm2song.py, the VGM-to-.song converter
|
||||
# check-vgm.sh - Host gate for tools/vgm2song, the VGM-to-.song converter
|
||||
# that brings tracker-made chip music (Furnace's, docs/Assets.adoc) to
|
||||
# JoeyLib's chip tracker.
|
||||
#
|
||||
# 1. tests/host/vgmCheck.py synthetic: VGM files written to known register
|
||||
# 1. tests/gate/vgmCheck.c synthetic: VGM files written to known register
|
||||
# writes, converted and baked; every baked row must hold the voices worked
|
||||
# out by hand.
|
||||
# 2. With Furnace installed (./install.sh install furnace): every SN76489 and
|
||||
# AY-3-8910 demo song Furnace comes with that uses that chip alone is
|
||||
# exported by Furnace to VGM and to audio (WAV), converted and baked; each
|
||||
# must bake, and Furnace's own audio must sound the pitches the baked song
|
||||
# plays (tests/host/vgmCheck.py pitch): at least PITCH_MIN_SONG percent of
|
||||
# plays (tests/gate/vgmCheck.c pitch): at least PITCH_MIN_SONG percent of
|
||||
# each song's pitched windows, PITCH_MIN_MEAN on average (a wrong clock or
|
||||
# divider gives under 10). The demos are their authors' and stay where the
|
||||
# installer put them; nothing made from them is kept.
|
||||
|
|
@ -34,8 +34,14 @@ DEMO_FOLDERS="sn7:sn ay8910:ay"
|
|||
work=$(mktemp -d -t joeylib-vgm.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
mkdir -p "$work/synthetic" "$work/demos" "$work/home"
|
||||
# The converter, the baker, and the checker, make/tools.mk's (vgmCheck finds
|
||||
# the first two in JOEY_BUILD's tools/ too).
|
||||
vgm2song=$(joeyTool vgm2song) || exit 1
|
||||
songbake=$(joeyTool songbake) || exit 1
|
||||
vgmCheck=$(joeyTool vgmCheck) || exit 1
|
||||
export JOEY_BUILD=${JOEY_BUILD:-$repo/build}
|
||||
|
||||
synthetic=$(python3 "$repo/tests/host/vgmCheck.py" synthetic "$repo" "$work/synthetic")
|
||||
synthetic=$("$vgmCheck" synthetic "$repo" "$work/synthetic")
|
||||
status=$?
|
||||
echo "$synthetic" | grep -v '^SYNTHETIC'
|
||||
counts=$(echo "$synthetic" | sed -n 's/^SYNTHETIC //p')
|
||||
|
|
@ -65,7 +71,7 @@ for spec in $DEMO_FOLDERS; do
|
|||
fails="${fails:+$fails; }$folder/$name: Furnace did not export it"
|
||||
continue
|
||||
fi
|
||||
if ! python3 "$repo/tools/vgm2song.py" --chip "$chip" "$base.vgm" "$base.song" 2> "$base.warn"; then
|
||||
if ! "$vgm2song" --chip "$chip" "$base.vgm" "$base.song" 2> "$base.warn"; then
|
||||
fails="${fails:+$fails; }$folder/$name: $(tr '\n' ' ' < "$base.warn")"
|
||||
continue
|
||||
fi
|
||||
|
|
@ -75,11 +81,11 @@ for spec in $DEMO_FOLDERS; do
|
|||
echo "$label: $folder/$name: skipped (the song uses another chip too)"
|
||||
continue
|
||||
fi
|
||||
if ! python3 "$repo/tools/songbake/songbake.py" "$base.song" "$base.jym" > /dev/null 2> "$base.bake"; then
|
||||
if ! "$songbake" "$base.song" "$base.jym" > /dev/null 2> "$base.bake"; then
|
||||
fails="${fails:+$fails; }$folder/$name: songbake: $(grep -v warning "$base.bake" | tr '\n' ' ')"
|
||||
continue
|
||||
fi
|
||||
result=$(python3 "$repo/tests/host/vgmCheck.py" pitch "$base.wav" "$base.jym" | sed -n 's/^PITCH //p')
|
||||
result=$("$vgmCheck" pitch "$base.wav" "$base.jym" | sed -n 's/^PITCH //p')
|
||||
matched=${result%%/*}
|
||||
tested=${result##*/}
|
||||
if [ -z "$tested" ] || [ "$tested" -eq 0 ]; then
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
# the ENV settings (NAME=VALUE) until its
|
||||
# joeylog.txt has a line starting UNTIL (empty:
|
||||
# until it exits) or SECONDS pass
|
||||
# beQmp WORK ARGS... scripts/lib/qmp.py on the VM's QMP socket
|
||||
# beQmp WORK ARGS... tests/gate/qmp.c on the VM's QMP socket
|
||||
# beStart WORK DISK [QEMU-ARGS...]
|
||||
# boot the VM with the job disk, on BE_DISPLAY
|
||||
# (default none: headless); sets BE_PID. A
|
||||
|
|
@ -92,27 +92,23 @@ beCrashed() {
|
|||
# beDesktopUp WORK -- BeOS R5's desktop is up in BEOS_VIDEO_MODE: the
|
||||
# screen is that size and Deskbar, which R5's boot starts last, shows its
|
||||
# logo in the top right corner (the boot shows a bare desktop for some
|
||||
# seconds before it).
|
||||
# seconds before it). The logo's red in the 16-bit mode, the corner looked
|
||||
# at, and how many of the logo's pixels show there with Deskbar up (about
|
||||
# 280).
|
||||
BE_LOGO_RED=(206 0 49)
|
||||
BE_LOGO_CORNER_W=120
|
||||
BE_LOGO_CORNER_H=24
|
||||
BE_LOGO_PIXELS_MIN=100
|
||||
beDesktopUp() {
|
||||
local probe
|
||||
local width
|
||||
local red
|
||||
beQmp "$1" screendump "$1/mode.png" 2> /dev/null || return 1
|
||||
python3 - "$1/mode.png" "$BEOS_VIDEO_MODE" <<'PY'
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
# The logo's red, in the 16-bit mode, and how many of its pixels show in
|
||||
# the corner when Deskbar is there (about 280).
|
||||
LOGO_RED = (206, 0, 49)
|
||||
CORNER_W = 120
|
||||
CORNER_H = 24
|
||||
LOGO_PIXELS_MIN = 100
|
||||
|
||||
width, height = (int(n) for n in sys.argv[2].split('x'))
|
||||
screen = Image.open(sys.argv[1]).convert('RGB')
|
||||
if screen.size != (width, height):
|
||||
sys.exit(1)
|
||||
corner = screen.crop((width - CORNER_W, 0, width, CORNER_H))
|
||||
sys.exit(0 if list(corner.getdata()).count(LOGO_RED) >= LOGO_PIXELS_MIN else 1)
|
||||
PY
|
||||
probe=$(joeyTool pngProbe) || return 1
|
||||
[ "$("$probe" size "$1/mode.png")" = "$BEOS_VIDEO_MODE" ] || return 1
|
||||
width=${BEOS_VIDEO_MODE%x*}
|
||||
red=$("$probe" count "$1/mode.png" $((width - BE_LOGO_CORNER_W)) 0 "$BE_LOGO_CORNER_W" "$BE_LOGO_CORNER_H" "${BE_LOGO_RED[@]}") || return 1
|
||||
[ "$red" -ge "$BE_LOGO_PIXELS_MIN" ]
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -195,7 +191,7 @@ beNeed() {
|
|||
beQmp() {
|
||||
local work="$1"
|
||||
shift
|
||||
python3 "$JOEY_REPO/scripts/lib/qmp.py" "$work/qmp.sock" "$@"
|
||||
"$(joeyTool qmp)" "$work/qmp.sock" "$@"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
"""checkDocsCode.py - fail when the built manual shows source code with a
|
||||
character that is not ASCII.
|
||||
|
||||
Asciidoctor's replacements turn `->` into an arrow, `...` into an ellipsis and
|
||||
a backtick next to an apostrophe into a curly quote, even inside a backtick
|
||||
code span, so code the reader copies out of the manual would not compile.
|
||||
Write such a span as `+text+`, which Asciidoctor leaves as it is.
|
||||
|
||||
Usage: scripts/lib/checkDocsCode.py MANUAL.html
|
||||
Exit: 0 every code span and block is ASCII; 1 otherwise (each one listed).
|
||||
"""
|
||||
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
|
||||
CODE = re.compile(r"<(code|pre)\b[^>]*>(.*?)</\1>", re.S)
|
||||
TAG = re.compile(r"<[^>]+>")
|
||||
SAMPLE_CHARS = 70
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__.strip().split("\n\n")[-1], file=sys.stderr)
|
||||
return 2
|
||||
page = open(sys.argv[1], encoding="utf-8").read()
|
||||
bad = 0
|
||||
for match in CODE.finditer(page):
|
||||
text = html.unescape(TAG.sub("", match.group(2)))
|
||||
glyphs = sorted({ch for ch in text if ord(ch) > 127})
|
||||
if glyphs:
|
||||
bad += 1
|
||||
names = " ".join(f"U+{ord(ch):04X}" for ch in glyphs)
|
||||
print(f"checkDocsCode: {names} in <{match.group(1)}>: {text[:SAMPLE_CHARS]!r}", file=sys.stderr)
|
||||
if bad:
|
||||
print(f"checkDocsCode: {bad} code span(s) or block(s) not ASCII; write them as `+text+`", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
sys.exit(main())
|
||||
|
|
@ -122,28 +122,39 @@ dvxFullScreen() {
|
|||
# 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
|
||||
awk -v desktop="$2" -v width="$DVX_VIDEO_W" -v height="$DVX_VIDEO_H" -v bpp="$DVX_VIDEO_BPP" '
|
||||
BEGIN {
|
||||
RS = "\r\n"
|
||||
sets["shell/desktop"] = desktop
|
||||
sets["video/width"] = width
|
||||
sets["video/height"] = height
|
||||
sets["video/bpp"] = bpp
|
||||
}
|
||||
{
|
||||
line = $0
|
||||
stripped = $0
|
||||
gsub(/^[ \t\r\n\v\f]+|[ \t\r\n\v\f]+$/, "", stripped)
|
||||
if (substr(stripped, 1, 1) == "[") {
|
||||
section = stripped
|
||||
gsub(/^[][]+|[][]+$/, "", section)
|
||||
section = tolower(section)
|
||||
} else if (index(stripped, "=") > 0 && substr(stripped, 1, 1) != ";") {
|
||||
key = substr(stripped, 1, index(stripped, "=") - 1)
|
||||
gsub(/^[ \t\r\n\v\f]+|[ \t\r\n\v\f]+$/, "", key)
|
||||
if (section == "shell" && tolower(key) == "welcome") {
|
||||
next
|
||||
}
|
||||
if (section == "sounds") {
|
||||
line = key " ="
|
||||
} else if ((section "/" tolower(key)) in sets) {
|
||||
line = key " = " sets[section "/" tolower(key)]
|
||||
}
|
||||
}
|
||||
out = out (NR > 1 ? "\r\n" : "") line
|
||||
}
|
||||
END {
|
||||
printf "%s%s", out, (RT == "" ? "" : "\r\n") > FILENAME
|
||||
}' "$1"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -186,3 +186,18 @@ joeyStop() {
|
|||
wait "$pid" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
# joeyLe16 FILE -- print the little-endian 16-bit number FILE starts with.
|
||||
joeyLe16() {
|
||||
od -An -tu2 -N2 --endian=little "$1" | tr -d ' '
|
||||
}
|
||||
|
||||
|
||||
# joeyTool NAME -- print the path of one of JoeyLib's host tools
|
||||
# (make/tools.mk's, in JOEY_BUILD's tools/), built first if it is not
|
||||
# current. Fails, with make's message, when it cannot be built.
|
||||
joeyTool() {
|
||||
make -s -f "$JOEY_REPO/make/tools.mk" "$1" > /dev/null || return 1
|
||||
printf '%s\n' "${JOEY_BUILD:-$JOEY_REPO/build}/tools/$1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Shared by the joystick gates that read examples/joy's screen
|
||||
# (scripts/verify-*-joy.sh): the steps, the check of each screenshot
|
||||
# (scripts/lib/joyScreen.py) and the tally. Source after joeyEnv.sh. The gate
|
||||
# (tests/gate/joyScreen.c) and the tally. Source after joeyEnv.sh. The gate
|
||||
# defines three functions first:
|
||||
#
|
||||
# joyShot PNG save a screenshot of JOY as PNG; non-zero if none
|
||||
|
|
@ -32,12 +32,14 @@
|
|||
# exit 1 on a failure
|
||||
#
|
||||
# The gate sets JOY_LABEL (its name, for the lines it prints) and JOY_WORK
|
||||
# (its scratch folder). JOY_SCALE (joyScreen.py --scale) is the stage's
|
||||
# scale in the screenshot, empty to let joyScreen.py find it;
|
||||
# JOY_SCREEN_ARGS are more joyScreen.py options (--st); JOY_SHOTS, if
|
||||
# (its scratch folder). JOY_SCALE (joyScreen --scale) is the stage's
|
||||
# scale in the screenshot, empty to let joyScreen find it;
|
||||
# JOY_SCREEN_ARGS are more joyScreen options (--st); JOY_SHOTS, if
|
||||
# set, keeps every screenshot there; JOY_PRESS_SECONDS is how long a change
|
||||
# takes to show; JOY_SHOT_TRIES is 1 for a gate whose screenshots were all
|
||||
# taken before the checks (they cannot be taken again).
|
||||
# takes to show. A read with a "?" in it (a dot caught between places) is
|
||||
# taken again JOY_RETRY_SECONDS later, up to JOY_SHOT_TRIES screenshots in
|
||||
# all, about two seconds; JOY_SHOT_TRIES is 1 for a gate whose screenshots
|
||||
# were all taken before the checks (they cannot be taken again).
|
||||
|
||||
JOY_REST="10000" # a stick connected, in the middle, buttons up
|
||||
JOY_ABSENT="00000" # no stick
|
||||
|
|
@ -45,7 +47,7 @@ JOY_REST_B="$JOY_REST" # stick B when nothing holds it
|
|||
JOY_SCALE="${JOY_SCALE:-}"
|
||||
JOY_SCREEN_ARGS=()
|
||||
JOY_PRESS_SECONDS="${JOY_PRESS_SECONDS:-1}"
|
||||
JOY_SHOT_TRIES="${JOY_SHOT_TRIES:-2}"
|
||||
JOY_SHOT_TRIES="${JOY_SHOT_TRIES:-5}"
|
||||
JOY_RETRY_SECONDS=0.5
|
||||
JOY_FAILS=""
|
||||
JOY_CHECKED=0
|
||||
|
|
@ -99,7 +101,7 @@ joyCheck() {
|
|||
joyFail "$1: no screenshot"
|
||||
return
|
||||
fi
|
||||
got=$(python3 "$JOEY_REPO/scripts/lib/joyScreen.py" "$png" "${args[@]}")
|
||||
got=$("$(joeyTool joyScreen)" "$png" "${args[@]}")
|
||||
[[ "${got% scale=*}" == *"?"* ]] || break
|
||||
done
|
||||
if [ -n "${JOY_SHOTS:-}" ]; then
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""joyScreen.py - read both joysticks' state off a screenshot of JOY.
|
||||
|
||||
JOY (examples/joy) shows each stick in a frame: a dot that sits in the
|
||||
middle at rest and moves DOT_TRAVEL stage pixels per full axis
|
||||
(jlJoystickX/Y * DOT_TRAVEL / 127, grey at rest, green away from it), two
|
||||
button squares (yellow while down) and a patch under them, green when the
|
||||
port reports a joystick and red when it does not. This finds the stage in
|
||||
a screenshot (on a desktop the window is wherever DVX put it, at the
|
||||
window's scale) by the two patches, and prints each stick's state as five
|
||||
characters: connected (1/0), X and Y (- 0 +: the dot at least half its
|
||||
travel left or up, in the middle, at least half its travel right or
|
||||
down), buttons 0 and 1 (1/0); '?' for anything else (a dot part way from
|
||||
the middle, a colour that is not JOY's). Half way, as DVX's own joystick
|
||||
tests judge an axis: an uncalibrated DVX axis ranges itself as it moves,
|
||||
so an emulated stick's full push reads a little short of the end.
|
||||
|
||||
joyScreen.py PNG [--scale S] [--st] [--at X,Y]
|
||||
|
||||
--st: the screen shows the plain Atari ST's 3-bit palette, which drops each
|
||||
channel's low bit (Hatari shows 3-bit n as the STe's 4-bit 2n). --at: the
|
||||
stage starts there on the screen (at the scale --scale gives, else 1), so
|
||||
the patches need not be seen: where something covers one (the PSP's
|
||||
keyboard), its stick's connected character is '?'.
|
||||
|
||||
Prints "A=<state> B=<state> scale=<S> dots=<A's x,y offset> <B's>"; exit 1
|
||||
when the stage is not found or a stick cannot be read.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# examples/joy/joy.c: its layout (stage pixels) and colours ($0RGB).
|
||||
STICK_LEFT = (24, 216)
|
||||
STICK_TOP = 16
|
||||
FRAME_SIZE = 80
|
||||
FRAME_INSET = 2
|
||||
DOT_SIZE = 8
|
||||
DOT_TRAVEL = (FRAME_SIZE - DOT_SIZE) // 2 - FRAME_INSET
|
||||
BUTTON_W = 24
|
||||
BUTTON_H = 16
|
||||
BUTTON_GAP = 4
|
||||
ROW_GAP = 6
|
||||
BUTTON_TOP = STICK_TOP + FRAME_SIZE + ROW_GAP
|
||||
STATUS_W = 80
|
||||
STATUS_H = 4
|
||||
STATUS_TOP = BUTTON_TOP + BUTTON_H + ROW_GAP
|
||||
COLOURS = {
|
||||
"background": 0x000, "frame": 0x444, "dotOff": 0x666, "dotOn": 0x0F0,
|
||||
"buttonOff": 0x333, "buttonOn": 0xFF0, "disconnected": 0xF00,
|
||||
}
|
||||
# The patch's green is the dot's (COLOR_CONNECTED = COLOR_DOT_ON).
|
||||
CONNECTED = "dotOn"
|
||||
# A 15-bit desktop is off by at most 8 a channel, the 6-bit VGA DAC by at
|
||||
# most 3; JOY's colours sit much further apart. (All are exact in DVX's
|
||||
# 256-colour desktop palette.)
|
||||
MAX_DIST = 3 * 9 ** 2
|
||||
SCALES = (1, 2, 3)
|
||||
UNKNOWN = "?"
|
||||
|
||||
|
||||
def rgb(v):
|
||||
return (((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17)
|
||||
|
||||
|
||||
# A plain ST's 3-bit palette keeps a channel's top three bits.
|
||||
ST_CHANNEL_MASK = 0xEEE
|
||||
|
||||
|
||||
def palette(mask):
|
||||
return {name: rgb(v & mask) for name, v in COLOURS.items()}
|
||||
|
||||
|
||||
class Screen:
|
||||
def __init__(self, img, colours):
|
||||
self.w, self.h = img.size
|
||||
self.pixels = list(img.getdata())
|
||||
self.colours = colours
|
||||
self.names = {}
|
||||
|
||||
def name(self, x, y):
|
||||
"""JOY's colour name at screen (x, y), or None."""
|
||||
if not (0 <= x < self.w and 0 <= y < self.h):
|
||||
return None
|
||||
c = self.pixels[y * self.w + x]
|
||||
if c not in self.names:
|
||||
best, bestDist = None, MAX_DIST + 1
|
||||
for n, p in self.colours.items():
|
||||
d = sum((a - b) ** 2 for a, b in zip(c, p))
|
||||
if d < bestDist:
|
||||
best, bestDist = n, d
|
||||
self.names[c] = best
|
||||
return self.names[c]
|
||||
|
||||
|
||||
def findStage(scr, scale):
|
||||
"""The screen position of stage (0, 0), found by the two status patches:
|
||||
runs of STATUS_W stage pixels in either patch colour on one row, as far
|
||||
apart as the two frames."""
|
||||
run = STATUS_W * scale
|
||||
gap = (STICK_LEFT[1] - STICK_LEFT[0]) * scale
|
||||
patch = (CONNECTED, "disconnected")
|
||||
for y in range(scr.h - STATUS_H * scale + 1):
|
||||
x = 0
|
||||
while x < scr.w:
|
||||
if scr.name(x, y) not in patch:
|
||||
x += 1
|
||||
continue
|
||||
start = x
|
||||
while x < scr.w and scr.name(x, y) in patch:
|
||||
x += 1
|
||||
if x - start != run:
|
||||
continue
|
||||
other = start + gap
|
||||
if all(scr.name(other + i, y) in patch for i in range(run)) and scr.name(other + run, y) not in patch:
|
||||
ox = start - STICK_LEFT[0] * scale
|
||||
oy = y - STATUS_TOP * scale
|
||||
if ox >= 0 and oy >= 0:
|
||||
return ox, oy
|
||||
return None
|
||||
|
||||
|
||||
def stagePixel(scr, origin, scale, x, y):
|
||||
"""JOY's colour name of stage pixel (x, y): its block's middle."""
|
||||
return scr.name(origin[0] + x * scale + scale // 2, origin[1] + y * scale + scale // 2)
|
||||
|
||||
|
||||
def axisChar(offset):
|
||||
if offset == 0:
|
||||
return "0"
|
||||
if offset <= -DOT_TRAVEL // 2:
|
||||
return "-"
|
||||
if offset >= DOT_TRAVEL // 2:
|
||||
return "+"
|
||||
return UNKNOWN
|
||||
|
||||
|
||||
def stick(scr, origin, scale, left):
|
||||
"""The five-character state of the stick whose frame starts at left, and
|
||||
its dot's offset from the middle ("x,y", "?" when there is no dot)."""
|
||||
patch = stagePixel(scr, origin, scale, left + STATUS_W // 2, STATUS_TOP + STATUS_H // 2)
|
||||
connected = {CONNECTED: "1", "disconnected": "0"}.get(patch, UNKNOWN)
|
||||
|
||||
dots = [(x, y, stagePixel(scr, origin, scale, x, y))
|
||||
for y in range(STICK_TOP, STICK_TOP + FRAME_SIZE)
|
||||
for x in range(left, left + FRAME_SIZE)]
|
||||
dots = [(x, y, n) for x, y, n in dots if n in ("dotOn", "dotOff")]
|
||||
ax = ay = UNKNOWN
|
||||
offset = UNKNOWN
|
||||
if dots:
|
||||
x0 = min(d[0] for d in dots)
|
||||
y0 = min(d[1] for d in dots)
|
||||
whole = len(dots) == DOT_SIZE * DOT_SIZE and max(d[0] for d in dots) == x0 + DOT_SIZE - 1 and max(d[1] for d in dots) == y0 + DOT_SIZE - 1
|
||||
colours = set(d[2] for d in dots)
|
||||
dx = x0 - (left + FRAME_SIZE // 2 - DOT_SIZE // 2)
|
||||
dy = y0 - (STICK_TOP + FRAME_SIZE // 2 - DOT_SIZE // 2)
|
||||
offset = "%+d,%+d" % (dx, dy)
|
||||
if whole and len(colours) == 1:
|
||||
ax = axisChar(dx)
|
||||
ay = axisChar(dy)
|
||||
# Green away from the middle, grey in it.
|
||||
atRest = ax == "0" and ay == "0"
|
||||
if colours != ({"dotOff"} if atRest else {"dotOn"}):
|
||||
ax = ay = UNKNOWN
|
||||
|
||||
centre = left + FRAME_SIZE // 2
|
||||
buttons = ""
|
||||
for bx in (centre - BUTTON_W - BUTTON_GAP // 2, centre + BUTTON_GAP // 2):
|
||||
n = stagePixel(scr, origin, scale, bx + BUTTON_W // 2, BUTTON_TOP + BUTTON_H // 2)
|
||||
buttons += {"buttonOn": "1", "buttonOff": "0"}.get(n, UNKNOWN)
|
||||
return connected + ax + ay + buttons, offset
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
scales = SCALES
|
||||
mask = 0xFFF
|
||||
at = None
|
||||
args = argv[2:]
|
||||
while args:
|
||||
if args[0] == "--scale" and len(args) > 1:
|
||||
scales = (int(args[1]),)
|
||||
args = args[2:]
|
||||
elif args[0] == "--st":
|
||||
mask = ST_CHANNEL_MASK
|
||||
args = args[1:]
|
||||
elif args[0] == "--at" and len(args) > 1:
|
||||
at = tuple(int(v) for v in args[1].split(","))
|
||||
args = args[2:]
|
||||
else:
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
|
||||
scr = Screen(Image.open(argv[1]).convert("RGB"), palette(mask))
|
||||
origin = None
|
||||
for scale in scales:
|
||||
origin = at if at else findStage(scr, scale)
|
||||
if origin:
|
||||
break
|
||||
if not origin:
|
||||
print("joyScreen: FAIL - JOY's two status patches are not on the screen")
|
||||
return 1
|
||||
read = [stick(scr, origin, scale, left) for left in STICK_LEFT]
|
||||
print("A=%s B=%s scale=%d dots=%s %s" % (read[0][0], read[1][0], scale, read[0][1], read[1][1]))
|
||||
return 1 if any(UNKNOWN in state for state, offset in read) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""keysCells.py - read which of KEYS's named-key cells are lit off a screenshot.
|
||||
|
||||
KEYS (examples/keys) draws a cell for each named key, lit (colour 2, green)
|
||||
while jlKeyDown says it is down and dark grey (colour 1) otherwise. This
|
||||
reads the cells of its fourth row that a port gives a second physical key
|
||||
for -- Return, Ctrl, Alt, Up, Down, Left, and Right -- at the middle of
|
||||
each, and prints one digit a cell, in that order: 1 lit, 0 not, ? neither
|
||||
colour.
|
||||
|
||||
keysCells.py PNG [--at X,Y] [--scale S]
|
||||
|
||||
--at: where the stage's top left is in the screenshot (default 0,0).
|
||||
--scale: screen pixels a stage pixel (default 1).
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# examples/keys: CELL_W x CELL_H cells GAP apart from (MARGIN_X, MARGIN_Y),
|
||||
# the named keys in row 4 at these columns.
|
||||
CELL_W = 28
|
||||
CELL_H = 26
|
||||
GAP = 4
|
||||
MARGIN_X = 2
|
||||
MARGIN_Y = 2
|
||||
ROW = 4
|
||||
COLUMNS = (2, 3, 4, 5, 6, 7, 8) # Return, Ctrl, Alt, Up, Down, Left, Right
|
||||
# KEYS's colours 1 and 2 (12-bit $333 and $0F0, each channel widened x17).
|
||||
UNLIT = (0x33, 0x33, 0x33)
|
||||
LIT = (0x00, 0xFF, 0x00)
|
||||
# A 15- or 16-bit screen is off by at most 8 a channel.
|
||||
MAX_DIST = 3 * 9 ** 2
|
||||
|
||||
|
||||
def near(got, want):
|
||||
return sum((g - w) ** 2 for g, w in zip(got, want)) <= MAX_DIST
|
||||
|
||||
|
||||
def main(argv):
|
||||
args = argv[1:]
|
||||
at = (0, 0)
|
||||
scale = 1
|
||||
png = None
|
||||
while args:
|
||||
arg = args.pop(0)
|
||||
if arg == "--at":
|
||||
at = tuple(int(v) for v in args.pop(0).split(","))
|
||||
elif arg == "--scale":
|
||||
scale = int(args.pop(0))
|
||||
else:
|
||||
png = arg
|
||||
if png is None:
|
||||
print(__doc__.strip(), file=sys.stderr)
|
||||
return 2
|
||||
image = Image.open(png).convert("RGB")
|
||||
y = MARGIN_Y + ROW * (CELL_H + GAP) + CELL_H // 2
|
||||
digits = []
|
||||
for column in COLUMNS:
|
||||
x = MARGIN_X + column * (CELL_W + GAP) + CELL_W // 2
|
||||
pixel = image.getpixel((at[0] + x * scale, at[1] + y * scale))
|
||||
digits.append("1" if near(pixel, LIT) else ("0" if near(pixel, UNLIT) else "?"))
|
||||
print("".join(digits))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
# go first, which must leave Up down (src/core/inputInternal.h,
|
||||
# INPUT_KEY_SECOND). Each step holds its keys (letting go of every other),
|
||||
# waits KEYS_SETTLE_SECONDS, and reads KEYS's Return, Ctrl, Alt, Up, Down,
|
||||
# Left, and Right cells (examples/keys) as seven digits, keysCells.py's.
|
||||
# Left, and Right cells (examples/keys) as seven digits, keysCells's.
|
||||
#
|
||||
# The gate sets KEYS_LABEL (its name, for the lines it prints) and defines,
|
||||
# by the key names below:
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""keysRow.py - read KEYS's verification row off a screenshot.
|
||||
|
||||
KEYS (examples/keys) draws every character it received on its row 198 as
|
||||
pixel nibbles: the sentinel A 5 A 5, the count byte, a zero byte, then each
|
||||
character, two pixels a byte, high nibble first. This finds that row in a
|
||||
screenshot (on a desktop the window is wherever DVX put it, at the window's
|
||||
scale: every stage pixel a SCALE x SCALE block) and checks it holds exactly
|
||||
TARGET, and that every block of it is whole: each of its screen pixels the
|
||||
same colour.
|
||||
|
||||
keysRow.py PNG TARGET [--size WxH] [--at X,Y] [--scale S]
|
||||
|
||||
--size: the screenshot must be that size (the full-screen mode, not the
|
||||
desktop). --at: the row must start exactly there (the stage's own row 198
|
||||
at 1x on a full screen). --scale: the row must be at that scale (else the
|
||||
first of 1, 2, 3 it is found at). Prints the row it found; exit 0 when it
|
||||
is right.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# KEYS palette 0 (buildPalette): a gray ramp with four demo colours and red
|
||||
# at 15, 12-bit $0RGB, each channel widened x17 as the DVX port's tables do.
|
||||
PAL12 = [(i << 8) | (i << 4) | i for i in range(16)]
|
||||
PAL12[0], PAL12[1], PAL12[2], PAL12[3] = 0x000, 0x333, 0x0F0, 0xFFF
|
||||
PAL12[15] = 0xF00
|
||||
PAL_RGB = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in PAL12]
|
||||
# A 15- or 16-bit desktop is off by at most 8 a channel after the round
|
||||
# trip, the 6-bit VGA DAC by at most 3, while the palette entries sit 17
|
||||
# apart: nearest is exact. (Every KEYS colour is also one of DVX's 256-colour
|
||||
# desktop palette, its cube or its grey ramp, so 8 bits is exact too.)
|
||||
MAX_DIST = 3 * 9 ** 2
|
||||
NO_NIBBLE = -1
|
||||
SENTINEL = (0xA, 0x5, 0xA, 0x5)
|
||||
SCALES = (1, 2, 3)
|
||||
|
||||
|
||||
def nibbleMap(img):
|
||||
"""Every pixel's KEYS palette index, NO_NIBBLE where no entry is near."""
|
||||
nearest = {}
|
||||
for rgb in set(img.getdata()):
|
||||
best, bestDist = NO_NIBBLE, MAX_DIST + 1
|
||||
for n, pal in enumerate(PAL_RGB):
|
||||
d = sum((c - p) ** 2 for c, p in zip(rgb, pal))
|
||||
if d < bestDist:
|
||||
best, bestDist = n, d
|
||||
nearest[rgb] = best
|
||||
return [nearest[rgb] for rgb in img.getdata()]
|
||||
|
||||
|
||||
def findRow(nibs, pixels, w, h, scale, expect):
|
||||
"""The (x, y, bytes) of the first row at scale whose header matches."""
|
||||
header = expect[:4]
|
||||
span = 2 * len(expect) * scale
|
||||
for y in range(h):
|
||||
base = y * w
|
||||
for x in range(w - span + 1):
|
||||
if nibs[base + x] != SENTINEL[0]:
|
||||
continue
|
||||
if tuple(nibs[base + x + i * scale] for i in range(len(SENTINEL))) != SENTINEL:
|
||||
continue
|
||||
row = bytes(((nibs[base + x + 2 * i * scale] & 0xF) << 4) | (nibs[base + x + (2 * i + 1) * scale] & 0xF) for i in range(len(expect)))
|
||||
if row[:4] == header:
|
||||
return x, y, row
|
||||
return None
|
||||
|
||||
|
||||
def splitBlocks(pixels, w, h, x, y, scale, count):
|
||||
"""How many of the count stage pixels from (x, y) are not whole blocks."""
|
||||
split = 0
|
||||
for i in range(count):
|
||||
left = x + i * scale
|
||||
want = pixels[y * w + left]
|
||||
block = [pixels[(y + dy) * w + left + dx] for dy in range(scale) if y + dy < h for dx in range(scale)]
|
||||
if len(block) != scale * scale or any(p != want for p in block):
|
||||
split += 1
|
||||
return split
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 3:
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
png, target = argv[1], argv[2]
|
||||
size = None
|
||||
at = None
|
||||
scales = SCALES
|
||||
args = argv[3:]
|
||||
while args:
|
||||
if args[0] == "--size" and len(args) > 1:
|
||||
size = tuple(int(n) for n in args[1].split("x"))
|
||||
elif args[0] == "--at" and len(args) > 1:
|
||||
at = tuple(int(n) for n in args[1].split(","))
|
||||
elif args[0] == "--scale" and len(args) > 1:
|
||||
scales = (int(args[1]),)
|
||||
else:
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
args = args[2:]
|
||||
|
||||
img = Image.open(png).convert("RGB")
|
||||
w, h = img.size
|
||||
if size is not None and img.size != size:
|
||||
print("keysRow: FAIL - %s is %dx%d, want %dx%d" % (png, w, h, size[0], size[1]))
|
||||
return 1
|
||||
|
||||
pixels = list(img.getdata())
|
||||
nibs = nibbleMap(img)
|
||||
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
|
||||
found = None
|
||||
for scale in scales:
|
||||
found = findRow(nibs, pixels, w, h, scale, expect)
|
||||
if found:
|
||||
break
|
||||
if not found:
|
||||
print("keysRow: FAIL - no verification row (A5A5 %02X00) on the screen at %s" % (len(target), " or ".join("%dx" % s for s in scales)))
|
||||
return 1
|
||||
x, y, row = found
|
||||
print("keysRow: row at (%d,%d) at %dx=%s" % (x, y, scale, row.hex().upper()))
|
||||
if row != expect:
|
||||
print("keysRow: FAIL - screen row %s, want %s" % (row.hex().upper(), expect.hex().upper()))
|
||||
return 1
|
||||
split = splitBlocks(pixels, w, h, x, y, scale, 2 * len(expect))
|
||||
if split:
|
||||
print("keysRow: FAIL - %d of the row's %d stage pixels are not whole %dx%d blocks" % (split, 2 * len(expect), scale, scale))
|
||||
return 1
|
||||
if at is not None and (x, y) != at:
|
||||
print("keysRow: FAIL - the row is at (%d,%d), want (%d,%d)" % (x, y, at[0], at[1]))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""mameNullModemCfg.py - a MAME cfg file that sets a null_modem's line.
|
||||
|
||||
MAME's null_modem clocks the wire itself, at 9600 8 N 1 unless its
|
||||
configuration says otherwise, so a guest whose UART runs at any other rate
|
||||
sees garbage. The line is a CONFIG input port on the device, which MAME
|
||||
reads back from cfg/SYSTEM.cfg in its -cfg_directory.
|
||||
|
||||
mameNullModemCfg.py SYSTEM TAG BAUD [DATABITS PARITY STOPBITS]
|
||||
|
||||
SYSTEM is the driver (x68000, apple2gs, apple2gsr1), TAG the null_modem's
|
||||
device tag (":rs232c:null_modem", ":modem:null_modem"). The line may also
|
||||
come as one quoted argument ("4800 7 E 1"). DATABITS PARITY STOPBITS
|
||||
default to 8 N 1. Prints the cfg; exit 2 for a line MAME cannot set.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# MAME's RS232_* enum values (src/devices/bus/rs232/rs232.h).
|
||||
BAUD = {"50": 14, "75": 15, "110": 0, "150": 1, "200": 17, "300": 2, "600": 3,
|
||||
"1200": 4, "1800": 18, "2000": 19, "2400": 5, "3600": 20, "4800": 6,
|
||||
"7200": 21, "9600": 7, "14400": 8, "19200": 9, "28800": 10,
|
||||
"38400": 11, "57600": 12, "115200": 13}
|
||||
DATA = {"5": 0, "6": 1, "7": 2, "8": 3}
|
||||
PARITY = {"N": 0, "O": 1, "E": 2}
|
||||
STOP = {"1": 1, "2": 3}
|
||||
DEFAULT_FORMAT = ["8", "N", "1"]
|
||||
# The null_modem's own defaults (src/devices/bus/rs232/null_modem.cpp).
|
||||
BAUD_DEFAULT = 7
|
||||
DATA_DEFAULT = 3
|
||||
PARITY_DEFAULT = 0
|
||||
STOP_DEFAULT = 1
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 4:
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
system = argv[1]
|
||||
tag = argv[2]
|
||||
line = " ".join(argv[3:]).split()
|
||||
if len(line) == 1:
|
||||
line += DEFAULT_FORMAT
|
||||
if len(line) != 4:
|
||||
sys.stderr.write("mameNullModemCfg.py: the line is BAUD [DATABITS PARITY STOPBITS]\n")
|
||||
return 2
|
||||
baud, data, parity, stop = line
|
||||
parity = parity.upper()
|
||||
for value, table, what in ((baud, BAUD, "baud rate"), (data, DATA, "data bits"),
|
||||
(parity, PARITY, "parity"), (stop, STOP, "stop bits")):
|
||||
if value not in table:
|
||||
sys.stderr.write("mameNullModemCfg.py: MAME's null_modem has no %s %s (it takes %s)\n"
|
||||
% (what, value, " ".join(table)))
|
||||
return 2
|
||||
ports = [("RS232_TXBAUD", BAUD_DEFAULT, BAUD[baud]), ("RS232_RXBAUD", BAUD_DEFAULT, BAUD[baud]),
|
||||
("RS232_DATABITS", DATA_DEFAULT, DATA[data]), ("RS232_PARITY", PARITY_DEFAULT, PARITY[parity]),
|
||||
("RS232_STOPBITS", STOP_DEFAULT, STOP[stop])]
|
||||
print('<?xml version="1.0"?>\n<mameconfig version="10">\n <system name="%s">\n <input>' % system)
|
||||
for name, default, value in ports:
|
||||
print(f' <port tag="{tag}:{name}" type="CONFIG" mask="255" defvalue="{default}" value="{value}" />')
|
||||
print(' </input>\n </system>\n</mameconfig>')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
82
scripts/lib/mameNullModemCfg.sh
Executable file
82
scripts/lib/mameNullModemCfg.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env bash
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# mameNullModemCfg.sh - a MAME cfg file that sets a null_modem's line.
|
||||
#
|
||||
# MAME's null_modem clocks the wire itself, at 9600 8 N 1 unless its
|
||||
# configuration says otherwise, so a guest whose UART runs at any other rate
|
||||
# sees garbage. The line is a CONFIG input port on the device, which MAME
|
||||
# reads back from cfg/SYSTEM.cfg in its -cfg_directory.
|
||||
#
|
||||
# mameNullModemCfg.sh SYSTEM TAG BAUD [DATABITS PARITY STOPBITS]
|
||||
#
|
||||
# SYSTEM is the driver (x68000, apple2gs, apple2gsr1), TAG the null_modem's
|
||||
# device tag (":rs232c:null_modem", ":modem:null_modem"). The line may also
|
||||
# come as one quoted argument ("4800 7 E 1"). DATABITS PARITY STOPBITS
|
||||
# default to 8 N 1. Prints the cfg; exit 2 for a line MAME cannot set.
|
||||
set -uo pipefail
|
||||
|
||||
label=mameNullModemCfg.sh
|
||||
# MAME's RS232_* enum values (src/devices/bus/rs232/rs232.h), and each
|
||||
# table's keys in the order a message lists them.
|
||||
declare -A BAUD=([50]=14 [75]=15 [110]=0 [150]=1 [200]=17 [300]=2 [600]=3
|
||||
[1200]=4 [1800]=18 [2000]=19 [2400]=5 [3600]=20 [4800]=6
|
||||
[7200]=21 [9600]=7 [14400]=8 [19200]=9 [28800]=10
|
||||
[38400]=11 [57600]=12 [115200]=13)
|
||||
BAUD_KEYS="50 75 110 150 200 300 600 1200 1800 2000 2400 3600 4800 7200 9600 14400 19200 28800 38400 57600 115200"
|
||||
declare -A DATA=([5]=0 [6]=1 [7]=2 [8]=3)
|
||||
DATA_KEYS="5 6 7 8"
|
||||
declare -A PARITY=([N]=0 [O]=1 [E]=2)
|
||||
PARITY_KEYS="N O E"
|
||||
declare -A STOP=([1]=1 [2]=3)
|
||||
STOP_KEYS="1 2"
|
||||
DEFAULT_FORMAT=(8 N 1)
|
||||
# The null_modem's own defaults (src/devices/bus/rs232/null_modem.cpp).
|
||||
BAUD_DEFAULT=7
|
||||
DATA_DEFAULT=3
|
||||
PARITY_DEFAULT=0
|
||||
STOP_DEFAULT=1
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "usage: $label SYSTEM TAG BAUD [DATABITS PARITY STOPBITS]" >&2
|
||||
exit 2
|
||||
fi
|
||||
system=$1
|
||||
tag=$2
|
||||
shift 2
|
||||
read -ra line <<< "$*"
|
||||
if [ "${#line[@]}" -eq 1 ]; then
|
||||
line+=("${DEFAULT_FORMAT[@]}")
|
||||
fi
|
||||
if [ "${#line[@]}" -ne 4 ]; then
|
||||
echo "$label: the line is BAUD [DATABITS PARITY STOPBITS]" >&2
|
||||
exit 2
|
||||
fi
|
||||
baud=${line[0]}
|
||||
data=${line[1]}
|
||||
parity=${line[2]^^}
|
||||
stop=${line[3]}
|
||||
|
||||
# check VALUE TABLE KEYS WHAT -- exit 2 unless TABLE has VALUE.
|
||||
check() {
|
||||
local -n table=$2
|
||||
if [ -z "${table[$1]+x}" ]; then
|
||||
echo "$label: MAME's null_modem has no $4 $1 (it takes $3)" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
check "$baud" BAUD "$BAUD_KEYS" "baud rate"
|
||||
check "$data" DATA "$DATA_KEYS" "data bits"
|
||||
check "$parity" PARITY "$PARITY_KEYS" "parity"
|
||||
check "$stop" STOP "$STOP_KEYS" "stop bits"
|
||||
printf '<?xml version="1.0"?>\n<mameconfig version="10">\n <system name="%s">\n <input>\n' "$system"
|
||||
for port in "RS232_TXBAUD $BAUD_DEFAULT ${BAUD[$baud]}" "RS232_RXBAUD $BAUD_DEFAULT ${BAUD[$baud]}" \
|
||||
"RS232_DATABITS $DATA_DEFAULT ${DATA[$data]}" "RS232_PARITY $PARITY_DEFAULT ${PARITY[$parity]}" \
|
||||
"RS232_STOPBITS $STOP_DEFAULT ${STOP[$stop]}"; do
|
||||
read -r name default value <<< "$port"
|
||||
printf ' <port tag="%s:%s" type="CONFIG" mask="255" defvalue="%s" value="%s" />\n' "$tag" "$name" "$default" "$value"
|
||||
done
|
||||
printf ' </input>\n </system>\n</mameconfig>\n'
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""netWmIcon.py - an X window's icon against the picture it should be.
|
||||
|
||||
A window's icon on X is its _NET_WM_ICON property: one or more pictures,
|
||||
each its width, its height and then its pixels as 32-bit ARGB, row after
|
||||
row. SDL3 sets it from SDL_SetWindowIcon; Wine from the window class's icon.
|
||||
xprop prints it as numbers with
|
||||
|
||||
xprop -id WINDOW -f _NET_WM_ICON 32c ' = $0+' _NET_WM_ICON > XPROP
|
||||
|
||||
and this compares the picture of the wanted size in that against WANT
|
||||
(any picture PIL reads; an .ico's 32-bit picture of that size). A pixel
|
||||
at least half opaque must be so in both, and then of the same colour,
|
||||
give or take TOLERANCE a gun (a system may round its own way).
|
||||
|
||||
netWmIcon.py XPROP WANT SIZE LABEL
|
||||
|
||||
Prints LABEL: PASS or LABEL: FAIL, and exits 0 or 1.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
TOLERANCE = 2
|
||||
ALPHA_OPAQUE_MIN = 128
|
||||
ICO_BPP = 32
|
||||
|
||||
|
||||
def pictures(text):
|
||||
"""Every (width, height, pixels) in xprop's numbers."""
|
||||
match = re.search(r"=\s*([\d,\s]+)", text)
|
||||
if match is None:
|
||||
return []
|
||||
values = [int(v) for v in match.group(1).replace(",", " ").split()]
|
||||
out = []
|
||||
at = 0
|
||||
while at + 2 <= len(values):
|
||||
w, h = values[at], values[at + 1]
|
||||
at += 2
|
||||
out.append((w, h, values[at:at + w * h]))
|
||||
at += w * h
|
||||
return out
|
||||
|
||||
|
||||
def wanted(path, size):
|
||||
"""WANT as RGBA at SIZE x SIZE: an .ico's 32-bit picture of that size."""
|
||||
image = Image.open(path)
|
||||
if image.format == "ICO":
|
||||
image = image.ico.getimage((size, size), ICO_BPP)
|
||||
return image.convert("RGBA")
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 5:
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
xpropPath, wantPath, size, label = argv[1], argv[2], int(argv[3]), argv[4]
|
||||
found = pictures(open(xpropPath, encoding="ascii", errors="replace").read())
|
||||
sizes = ", ".join("%dx%d" % (w, h) for w, h, _ in found) or "none"
|
||||
shown = [pixels for w, h, pixels in found if w == size and h == size]
|
||||
if not shown:
|
||||
print("%s: FAIL - the window has no %dx%d icon (it has: %s)" % (label, size, size, sizes))
|
||||
return 1
|
||||
want = wanted(wantPath, size)
|
||||
if want.size != (size, size):
|
||||
print("%s: FAIL - %s is %dx%d, not %dx%d" % (label, wantPath, want.size[0], want.size[1], size, size))
|
||||
return 1
|
||||
wrong = 0
|
||||
opaqueCount = 0
|
||||
for argb, (r, g, b, a) in zip(shown[0], want.getdata()):
|
||||
shownOpaque = (argb >> 24) >= ALPHA_OPAQUE_MIN
|
||||
if shownOpaque != (a >= ALPHA_OPAQUE_MIN):
|
||||
wrong += 1
|
||||
continue
|
||||
if not shownOpaque:
|
||||
continue
|
||||
opaqueCount += 1
|
||||
rgb = ((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF)
|
||||
if max(abs(rgb[0] - r), abs(rgb[1] - g), abs(rgb[2] - b)) > TOLERANCE:
|
||||
wrong += 1
|
||||
if wrong or opaqueCount == 0:
|
||||
print("%s: FAIL - %d of the %dx%d icon's pixels differ from %s (%d opaque)" % (label, wrong, size, size, wantPath, opaqueCount))
|
||||
return 1
|
||||
print("%s: PASS (the window's %dx%d icon is the baked one: %d opaque pixels and the transparency round them; icons on the window: %s)" % (label, size, size, opaqueCount, sizes))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Shared by the joystick gates of the ports that read the host's gamepads
|
||||
# (SDL3, Win32 under Wine): two virtual Xbox 360 pads
|
||||
# (scripts/lib/virtualPad.py) as sticks A and B, or two generic joysticks
|
||||
# (tests/gate/virtualPad.c) as sticks A and B, or two generic joysticks
|
||||
# no gamepad mapping knows. The pads are the whole machine's while they
|
||||
# exist, so a real pad plugged in here would be seen too: PAD_ONLY_ENV is
|
||||
# the environment that hides every other pad from SDL, as a gamepad and as
|
||||
|
|
@ -30,7 +30,7 @@ PAD_SENT=0
|
|||
PAD_SWAP=0
|
||||
PAD_READY_SECONDS=10
|
||||
PAD_REPLY_SECONDS=5
|
||||
# The virtual pads' USB vendor/product (virtualPad.py's), by kind.
|
||||
# The virtual pads' USB vendor/product (virtualPad's), by kind.
|
||||
PAD_IDS_XBOX360=0x045e/0x028e
|
||||
PAD_IDS_GENERIC=0x1209/0x0001
|
||||
PAD_ONLY_ENV=(SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=$PAD_IDS_XBOX360)
|
||||
|
|
@ -58,7 +58,7 @@ padOrder() {
|
|||
padSend press A-left
|
||||
sleep "$JOY_PRESS_SECONDS"
|
||||
joyShot "$JOY_WORK/order.png"
|
||||
got=$(python3 "$JOEY_REPO/scripts/lib/joyScreen.py" "$JOY_WORK/order.png" "${JOY_SCREEN_ARGS[@]}")
|
||||
got=$("$(joeyTool joyScreen)" "$JOY_WORK/order.png" "${JOY_SCREEN_ARGS[@]}")
|
||||
padSend release A-left
|
||||
sleep "$JOY_PRESS_SECONDS"
|
||||
case "$got" in
|
||||
|
|
@ -93,6 +93,7 @@ padSend() {
|
|||
padStart() {
|
||||
local fifo="$1/pad.in"
|
||||
local kind=()
|
||||
local pad
|
||||
if [ ! -w /dev/uinput ]; then
|
||||
echo "$JOY_LABEL: cannot write /dev/uinput, which the virtual gamepads need (a desktop session's user usually can)" >&2
|
||||
exit 2
|
||||
|
|
@ -107,7 +108,8 @@ padStart() {
|
|||
PAD_SWAP=0
|
||||
rm -f "$fifo" "$PAD_OUT"
|
||||
mkfifo "$fifo"
|
||||
python3 "$JOEY_REPO/scripts/lib/virtualPad.py" "${kind[@]}" "JoeyLib gate" < "$fifo" > "$PAD_OUT" 2>&1 &
|
||||
pad=$(joeyTool virtualPad) || exit 1
|
||||
"$pad" "${kind[@]}" "JoeyLib gate" < "$fifo" > "$PAD_OUT" 2>&1 &
|
||||
PAD_PID=$!
|
||||
exec {PAD_FD}> "$fifo"
|
||||
if ! padWait 0 "$PAD_READY_SECONDS"; then
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""pspAudioGrade.py - grade what AUDIOPRB played on the PSP.
|
||||
|
||||
AUDIOPRB is examples/audio built to press its own keys (make/examples.mk):
|
||||
the tracker song from the start, then S every PROBE_STREAM_FRAMES frames
|
||||
from PROBE_STREAM_FIRST (and N once, whose noise sweep the streams cover).
|
||||
Each S streams a falling square wave, each take STREAM_TAKE_HZ higher than
|
||||
the last. scripts/verify-psp-audio.sh records what reached the PSP's audio
|
||||
output (PPSSPP's own recording, 44.1 kHz stereo on the emulated clock), and
|
||||
this grades it from the moment the music starts:
|
||||
|
||||
music the tracker song alone, before the first take: sound, and
|
||||
quieter than a take
|
||||
take N each of the first three takes, a little way in: loud, and at
|
||||
its pitch, from the zero crossings -- so the PSP plays the mix
|
||||
at the rate it was made for
|
||||
end the recording stops within END_SLACK seconds of the program
|
||||
being told to quit: it ran until then, and ended on it
|
||||
|
||||
pspAudioGrade.py LABEL WAV QUIT_SECONDS
|
||||
|
||||
QUIT_SECONDS is when (since the recording began) the gate pressed Select,
|
||||
which the PSP port gives as Escape. Prints one line per check and PASS or
|
||||
FAIL; exit 1 on a failure.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "tools"))
|
||||
from gradeAudioPhases import readMono, rms # noqa: E402
|
||||
|
||||
# examples/audio/audio.c: the probe's timetable and the streamed tone.
|
||||
FRAME_HZ = 60
|
||||
PROBE_STREAM_FIRST = 30
|
||||
PROBE_STREAM_FRAMES = 90
|
||||
STREAM_RATE_HZ = 8000
|
||||
STREAM_BLOCK = 256
|
||||
STREAM_BLOCKS = 48
|
||||
STREAM_START_HZ = 330
|
||||
STREAM_DROP_HZ = 220
|
||||
STREAM_TAKE_HZ = 55
|
||||
TAKES = 3
|
||||
|
||||
# The music starts when its first sample rises above ONSET_LEVEL.
|
||||
ONSET_LEVEL = 200
|
||||
MUSIC_WINDOW = (0.05, 0.40)
|
||||
MUSIC_RMS = (1000, 8000)
|
||||
# A take is measured from TAKE_WINDOW seconds after it is due to start: far
|
||||
# enough in to miss the audio queue's latency, early in its fall.
|
||||
TAKE_WINDOW = (0.10, 0.20)
|
||||
TAKE_MIN_RMS = 15000
|
||||
PITCH_TOLERANCE = 0.10
|
||||
END_SLACK = 0.5
|
||||
|
||||
|
||||
def crossingsPerSecond(samples, rate, start, end):
|
||||
a = int(start * rate)
|
||||
b = min(int(end * rate), len(samples))
|
||||
count = 0
|
||||
for i in range(a + 1, b):
|
||||
if (samples[i - 1] < 0) != (samples[i] < 0):
|
||||
count += 1
|
||||
return count / ((b - a) / rate)
|
||||
|
||||
|
||||
def takeHz(take, seconds):
|
||||
"""The pitch take is at, seconds after it starts."""
|
||||
block = int(seconds * STREAM_RATE_HZ) // STREAM_BLOCK
|
||||
return STREAM_START_HZ + STREAM_TAKE_HZ * take - STREAM_DROP_HZ * block / STREAM_BLOCKS
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 4:
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
label, path, quitSeconds = argv[1], argv[2], float(argv[3])
|
||||
samples, rate = readMono(path)
|
||||
duration = len(samples) / rate
|
||||
onset = next((i / rate for i, v in enumerate(samples) if abs(v) > ONSET_LEVEL), None)
|
||||
if onset is None:
|
||||
print(f"{label}: FAIL - {duration:.1f}s recorded and all of it silent", file=sys.stderr)
|
||||
return 1
|
||||
fails = []
|
||||
|
||||
level = rms(samples, rate, onset + MUSIC_WINDOW[0], onset + MUSIC_WINDOW[1])
|
||||
print(f"{label}: music from {onset:.2f}s, RMS {level:.0f}")
|
||||
if level is None or not MUSIC_RMS[0] <= level <= MUSIC_RMS[1]:
|
||||
fails.append(f"music RMS {level}, want {MUSIC_RMS[0]}-{MUSIC_RMS[1]}")
|
||||
|
||||
for take in range(TAKES):
|
||||
due = onset + (PROBE_STREAM_FIRST + PROBE_STREAM_FRAMES * take) / FRAME_HZ
|
||||
start = due + TAKE_WINDOW[0]
|
||||
end = due + TAKE_WINDOW[1]
|
||||
level = rms(samples, rate, start, end)
|
||||
if level is None:
|
||||
fails.append(f"take {take + 1} is past the end of the recording")
|
||||
continue
|
||||
hz = crossingsPerSecond(samples, rate, start, end) / 2
|
||||
want = takeHz(take, (TAKE_WINDOW[0] + TAKE_WINDOW[1]) / 2)
|
||||
print(f"{label}: take {take + 1} at {start:.2f}s: RMS {level:.0f}, {hz:.0f} Hz (want {want:.0f})")
|
||||
if level < TAKE_MIN_RMS:
|
||||
fails.append(f"take {take + 1} RMS {level:.0f}, want at least {TAKE_MIN_RMS}")
|
||||
if abs(hz - want) > want * PITCH_TOLERANCE:
|
||||
fails.append(f"take {take + 1} at {hz:.0f} Hz, want {want:.0f}")
|
||||
|
||||
print(f"{label}: the recording ends at {duration:.2f}s, Select at {quitSeconds:.2f}s")
|
||||
if abs(duration - quitSeconds) > END_SLACK:
|
||||
fails.append(f"the recording ends at {duration:.2f}s, not within {END_SLACK}s of Select at {quitSeconds:.2f}s")
|
||||
|
||||
for f in fails:
|
||||
print(f"{label}: FAIL - {f}", file=sys.stderr)
|
||||
if fails:
|
||||
return 1
|
||||
print(f"{label}: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""pspKeyboardPath.py - the PSP keyboard's d-pad moves and presses for some
|
||||
typing, for a gate to send the PSP.
|
||||
|
||||
The keyboard (src/hosted/psp/pspKeyboard.c) is read from its own source:
|
||||
its rows of KEY_* entries, each key's width in half keys, what it types and
|
||||
its legend, and where the cursor starts. Its cursor is followed as the
|
||||
keyboard moves it -- along a row wrapping round, and up or down a row onto
|
||||
the key nearest the middle of the one it was on (ties: down-right going
|
||||
down, up-left going up). Each step is printed on a line of its own:
|
||||
|
||||
move left|right|up|down one d-pad press
|
||||
press LEGEND cross on the key with that legend
|
||||
|
||||
pspKeyboardPath.py SOURCE TOKEN...
|
||||
|
||||
A TOKEN is "key:LEGEND" (a key by its legend: ESC, F5, SHIFT, ENTER...) or
|
||||
"type:TEXT" (each character of TEXT, Shift pressed first for a shifted one,
|
||||
which the keyboard lets go after the next key).
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def chars(text):
|
||||
"""A C character literal's character ('\\'' and '\\\\' unescaped)."""
|
||||
text = text.strip()
|
||||
match = re.fullmatch(r"\(uint8_t\)\((.*)\)", text)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
if text.startswith("'") and text.endswith("'"):
|
||||
body = text[1:-1]
|
||||
return body[1] if body.startswith("\\") else body
|
||||
return None
|
||||
|
||||
|
||||
def args(call):
|
||||
"""The arguments of a KEY_*(...) entry, split at top-level commas."""
|
||||
out = []
|
||||
depth = 0
|
||||
quote = None
|
||||
current = ""
|
||||
i = 0
|
||||
while i < len(call):
|
||||
ch = call[i]
|
||||
if quote:
|
||||
current += ch
|
||||
if ch == "\\":
|
||||
current += call[i + 1]
|
||||
i += 1
|
||||
elif ch == quote:
|
||||
quote = None
|
||||
elif ch in "'\"":
|
||||
quote = ch
|
||||
current += ch
|
||||
elif ch == "(":
|
||||
depth += 1
|
||||
current += ch
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
current += ch
|
||||
elif ch == "," and depth == 0:
|
||||
out.append(current.strip())
|
||||
current = ""
|
||||
else:
|
||||
current += ch
|
||||
i += 1
|
||||
out.append(current.strip())
|
||||
return out
|
||||
|
||||
|
||||
def width(text):
|
||||
return int(text.rstrip("u"))
|
||||
|
||||
|
||||
def keyboard(source):
|
||||
"""The rows (each a list of keys: halves, legend, plain, shifted) and the
|
||||
cursor's starting row and column."""
|
||||
text = open(source).read()
|
||||
defines = dict(re.findall(r"#define (CURSOR_(?:ROW|COL)_START)\s+(\d+)u", text))
|
||||
rows = []
|
||||
for body in re.findall(r"static const BoardKeyT gRow\d+\[\] = \{(.*?)\n\};", text, re.S):
|
||||
row = []
|
||||
for kind, call in re.findall(r"(KEY_[A-Z]+)\((.*)\),?\s*$", body, re.M):
|
||||
a = args(call)
|
||||
if kind == "KEY_LETTER":
|
||||
row.append((2, a[0], chars(a[1]), a[0]))
|
||||
elif kind == "KEY_DIGIT":
|
||||
row.append((2, a[0], chars(a[1]), chars(a[2])))
|
||||
elif kind == "KEY_PUNCT":
|
||||
row.append((width(a[0]), chars(a[1]), chars(a[1]), chars(a[2])))
|
||||
elif kind == "KEY_NAMED":
|
||||
plain = chars(a[2])
|
||||
row.append((width(a[0]), a[3].strip('"'), plain, plain))
|
||||
elif kind == "KEY_LATCH":
|
||||
row.append((width(a[0]), a[3].strip('"'), None, None))
|
||||
rows.append(row)
|
||||
return rows, int(defines["CURSOR_ROW_START"]), int(defines["CURSOR_COL_START"])
|
||||
|
||||
|
||||
def center(row, col):
|
||||
"""Twice the middle of a key, in half keys."""
|
||||
start = sum(k[0] for k in row[:col])
|
||||
return 2 * start + row[col][0]
|
||||
|
||||
|
||||
def path(rows, start, target):
|
||||
"""Moves from start (row, col) to target, and the cursor's place after
|
||||
each, as the keyboard makes them: rows first, then along the row."""
|
||||
moves = []
|
||||
r, c = start
|
||||
tr, tc = target
|
||||
while r != tr:
|
||||
middle = center(rows[r], c)
|
||||
down = (tr - r) % len(rows) <= (r - tr) % len(rows)
|
||||
r = (r + 1) % len(rows) if down else (r - 1) % len(rows)
|
||||
best = None
|
||||
for col in range(len(rows[r])):
|
||||
d = abs(center(rows[r], col) - middle)
|
||||
if best is None or d < best or (down and d == best):
|
||||
best, c = d, col
|
||||
moves.append("move " + ("down" if down else "up"))
|
||||
count = len(rows[r])
|
||||
right = (tc - c) % count
|
||||
left = (c - tc) % count
|
||||
moves += ["move right"] * right if right <= left else ["move left"] * left
|
||||
return moves, (r, tc)
|
||||
|
||||
|
||||
def find(rows, want, byLegend):
|
||||
"""(row, col) of the key with legend want, or else of the key that types
|
||||
want; and whether that is its shifted character."""
|
||||
for r, row in enumerate(rows):
|
||||
for c, key in enumerate(row):
|
||||
if byLegend and key[1] == want:
|
||||
return (r, c), False
|
||||
if not byLegend and key[2] == want:
|
||||
return (r, c), False
|
||||
for r, row in enumerate(rows):
|
||||
for c, key in enumerate(row):
|
||||
if not byLegend and key[3] == want:
|
||||
return (r, c), True
|
||||
raise SystemExit("pspKeyboardPath: no key shows or types %r" % want)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
raise SystemExit(__doc__)
|
||||
rows, r0, c0 = keyboard(sys.argv[1])
|
||||
at = (r0, c0)
|
||||
for token in sys.argv[2:]:
|
||||
kind, _, value = token.partition(":")
|
||||
wants = [value] if kind == "key" else list(value)
|
||||
for want in wants:
|
||||
spot, shifted = find(rows, want, kind == "key")
|
||||
if shifted:
|
||||
shiftSpot, _ = find(rows, "SHIFT", True)
|
||||
moves, at = path(rows, at, shiftSpot)
|
||||
print("\n".join(moves + ["press SHIFT"]))
|
||||
moves, at = path(rows, at, spot)
|
||||
print("\n".join(moves + ["press " + rows[at[0]][at[1]][1]]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""qmp.py - drive a QEMU machine through its QMP socket.
|
||||
|
||||
The Be API ports' gates (scripts/lib/beGate.sh) take the screen and press keys
|
||||
this way, as a person at the machine would: the keys arrive through the
|
||||
emulated PS/2 keyboard.
|
||||
|
||||
qmp.py SOCKET screendump FILE.png the screen, as QEMU's display has it
|
||||
qmp.py SOCKET type TEXT type TEXT (US layout, shift as needed)
|
||||
qmp.py SOCKET keys KEY... press the KEYs, one after another
|
||||
qmp.py SOCKET hold KEY MS hold one key down for MS milliseconds
|
||||
qmp.py SOCKET down KEY press one key, and leave it down
|
||||
qmp.py SOCKET up KEY let go of it
|
||||
qmp.py SOCKET move DX DY move the mouse DX, DY (relative, as a PS/2
|
||||
mouse moves, in steps a PS/2 packet holds;
|
||||
the system stops it at the screen's edge)
|
||||
qmp.py SOCKET quit end QEMU
|
||||
|
||||
KEY is a QEMU key name (a qcode: esc, ret, spc, a, 1, ...).
|
||||
"""
|
||||
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
# How long each typed key is held, and the pause after it: slow enough that
|
||||
# a program reading its input once a frame sees every key.
|
||||
TYPE_HOLD_MS = 100
|
||||
TYPE_GAP_SECONDS = 0.15
|
||||
MS_PER_SECOND = 1000.0
|
||||
# A mouse move goes in steps of at most this many counts on each axis, with
|
||||
# this pause after each: BeOS R5's mouse driver gets a single long move wrong.
|
||||
MOVE_STEP_MAX = 100
|
||||
MOVE_GAP_SECONDS = 0.02
|
||||
|
||||
# Characters to (qcode, shifted) on a US keyboard.
|
||||
SHIFTED_DIGITS = ')!@#$%^&*('
|
||||
PUNCTUATION = {
|
||||
' ': ('spc', False), '.': ('dot', False), ',': ('comma', False),
|
||||
'/': ('slash', False), ';': ('semicolon', False), "'": ('apostrophe', False),
|
||||
'-': ('minus', False), '=': ('equal', False), '[': ('bracket_left', False),
|
||||
']': ('bracket_right', False), '\\': ('backslash', False), '`': ('grave_accent', False),
|
||||
':': ('semicolon', True), '?': ('slash', True), '>': ('dot', True), '<': ('comma', True),
|
||||
'"': ('apostrophe', True), '_': ('minus', True), '+': ('equal', True),
|
||||
'{': ('bracket_left', True), '}': ('bracket_right', True), '|': ('backslash', True),
|
||||
'~': ('grave_accent', True),
|
||||
}
|
||||
|
||||
|
||||
class Qmp:
|
||||
"""One QMP connection: the greeting read and capabilities negotiated."""
|
||||
|
||||
def __init__(self, path):
|
||||
self.sock = socket.socket(socket.AF_UNIX)
|
||||
self.sock.connect(path)
|
||||
self.file = self.sock.makefile('rw')
|
||||
self.file.readline()
|
||||
self.execute('qmp_capabilities')
|
||||
|
||||
def execute(self, command, arguments=None):
|
||||
"""Run one command; its return value. Events in between are skipped."""
|
||||
request = {'execute': command}
|
||||
if arguments is not None:
|
||||
request['arguments'] = arguments
|
||||
self.file.write(json.dumps(request) + '\n')
|
||||
self.file.flush()
|
||||
while True:
|
||||
reply = json.loads(self.file.readline())
|
||||
if 'error' in reply:
|
||||
raise RuntimeError('%s: %s' % (command, reply['error'].get('desc', reply['error'])))
|
||||
if 'return' in reply:
|
||||
return reply['return']
|
||||
|
||||
def keys(self, qcodes, holdMs):
|
||||
"""Press qcodes together, hold them holdMs, let them go."""
|
||||
self.execute('send-key', {
|
||||
'keys': [{'type': 'qcode', 'data': code} for code in qcodes],
|
||||
'hold-time': holdMs,
|
||||
})
|
||||
|
||||
|
||||
def charKey(ch):
|
||||
"""(qcode, shifted) for one character."""
|
||||
if 'a' <= ch <= 'z' or '0' <= ch <= '9':
|
||||
return ch, False
|
||||
if 'A' <= ch <= 'Z':
|
||||
return ch.lower(), True
|
||||
if ch in SHIFTED_DIGITS:
|
||||
return str(SHIFTED_DIGITS.index(ch)), True
|
||||
if ch in PUNCTUATION:
|
||||
return PUNCTUATION[ch]
|
||||
raise ValueError('no key types %r' % ch)
|
||||
|
||||
|
||||
def key(qmp, code, down):
|
||||
"""Press or let go of one key."""
|
||||
qmp.execute('input-send-event', {'events': [
|
||||
{'type': 'key', 'data': {'down': down, 'key': {'type': 'qcode', 'data': code}}},
|
||||
]})
|
||||
|
||||
|
||||
def move(qmp, dx, dy):
|
||||
"""Move the mouse dx, dy in MOVE_STEP_MAX steps."""
|
||||
steps = max(1, -(-max(abs(dx), abs(dy)) // MOVE_STEP_MAX))
|
||||
for step in range(steps):
|
||||
stepX = dx * (step + 1) // steps - dx * step // steps
|
||||
stepY = dy * (step + 1) // steps - dy * step // steps
|
||||
qmp.execute('input-send-event', {'events': [
|
||||
{'type': 'rel', 'data': {'axis': 'x', 'value': stepX}},
|
||||
{'type': 'rel', 'data': {'axis': 'y', 'value': stepY}},
|
||||
]})
|
||||
time.sleep(MOVE_GAP_SECONDS)
|
||||
|
||||
|
||||
def tap(qmp, qcodes):
|
||||
"""Press qcodes together for a typed key's time, then pause after them."""
|
||||
qmp.keys(qcodes, TYPE_HOLD_MS)
|
||||
time.sleep(TYPE_HOLD_MS / MS_PER_SECOND + TYPE_GAP_SECONDS)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 3:
|
||||
sys.exit(__doc__)
|
||||
qmp = Qmp(argv[1])
|
||||
command = argv[2]
|
||||
if command == 'screendump' and len(argv) == 4:
|
||||
qmp.execute('screendump', {'filename': argv[3], 'format': 'png'})
|
||||
elif command == 'type' and len(argv) == 4:
|
||||
for ch in argv[3]:
|
||||
code, shifted = charKey(ch)
|
||||
tap(qmp, ['shift', code] if shifted else [code])
|
||||
elif command == 'keys' and len(argv) >= 4:
|
||||
for code in argv[3:]:
|
||||
tap(qmp, [code])
|
||||
elif command == 'hold' and len(argv) == 5:
|
||||
holdMs = int(argv[4])
|
||||
qmp.keys([argv[3]], holdMs)
|
||||
time.sleep(holdMs / MS_PER_SECOND)
|
||||
elif command in ('down', 'up') and len(argv) == 4:
|
||||
key(qmp, argv[3], command == 'down')
|
||||
elif command == 'move' and len(argv) == 5:
|
||||
move(qmp, int(argv[3]), int(argv[4]))
|
||||
elif command == 'quit' and len(argv) == 3:
|
||||
qmp.execute('quit')
|
||||
else:
|
||||
sys.exit(__doc__)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""serialLoopPeer.py - the host end of the serial loopback gates.
|
||||
|
||||
The guest runs SERTEST (examples/sertest): it sends a banner, then echoes
|
||||
every byte it receives. This peer waits for the banner (the guest's
|
||||
transmit works), then sends a probe line and expects it back (its receive
|
||||
works too).
|
||||
|
||||
serialLoopPeer.py tcp-server PORT WALL [BANNER] listen; the emulator connects
|
||||
serialLoopPeer.py tcp-client PORT WALL [BANNER] connect to the emulator
|
||||
serialLoopPeer.py pty PATHFILE WALL [BANNER] make a raw pty pair, write the
|
||||
guest end's path to PATHFILE
|
||||
|
||||
WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP FAIL
|
||||
line; exit 0 on pass, 1 on fail.
|
||||
|
||||
--close-hold FILE (pty) then has SERTEST close the port: its DTR check, run
|
||||
for the closes alone (a pty has no modem lines), every marker line
|
||||
acknowledged, after which the port stays closed. Prints one CLOSE PASS /
|
||||
CLOSE FAIL line more, and keeps the pty pair open until FILE exists or WALL
|
||||
runs out, so the gate can read the guest end's settings after the close
|
||||
(verify-sdl3-serial.sh).
|
||||
|
||||
--stream N --rate RATE (TCP, nullmodem transparent:1) then streams N bytes of printable
|
||||
ASCII through SERTEST's echo, at most STREAM_WINDOW bytes ahead of what has
|
||||
come back, and checks every byte returns once, in order. Prints one STREAM
|
||||
PASS / STREAM FAIL line more, with the round trip's bytes a second; PASS
|
||||
needs that to be at least STREAM_MIN_PERCENT of what the line rate RATE
|
||||
(bits a second) carries.
|
||||
|
||||
--dtr-check (TCP, with DOSBox's nullmodem in transparent:0 mode, which puts
|
||||
the guest's modem lines in the byte stream as they change: 0xFF 0xFF is a
|
||||
data 0xFF, 0xFF <b> the lines, bit 0 RTS, bit 1 DTR) then runs SERTEST's
|
||||
DTR check: it sends DTR_CHECK, acknowledges each of SERTEST's marker lines
|
||||
(so SERTEST changes a line only once the marker before has gone out whole),
|
||||
and reads the line states against the markers. jlSerialSetDtr(false) and
|
||||
(true) must reach the line, closing and opening the port again must keep
|
||||
DTR up, and after a hang-up (DTR dropped, then the port closed) DTR must
|
||||
stay down for HANGUP_WATCH_SECONDS. Prints one DTR PASS / DTR FAIL line
|
||||
more.
|
||||
"""
|
||||
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import tty
|
||||
|
||||
DEFAULT_BANNER = b"JLSERTEST-READY"
|
||||
PROBE = b"PING12345"
|
||||
RESEND_SECONDS = 3.0 # the probe is re-sent until it comes back
|
||||
POLL_SECONDS = 0.5
|
||||
CONNECT_RETRY_SECONDS = 0.5
|
||||
READ_SIZE = 256
|
||||
SHOWN_BYTES = 80
|
||||
|
||||
# --stream: what the window, the chunks, and the pass mark are, and the bytes
|
||||
# it sends (printable ASCII, so never SERTEST's DTR_CHECK).
|
||||
STREAM_WINDOW = 1024
|
||||
STREAM_CHUNK = 64
|
||||
STREAM_POLL_SECONDS = 0.01
|
||||
STREAM_SETTLE_SECONDS = 1.0
|
||||
STREAM_MIN_PERCENT = 75
|
||||
STREAM_FIRST_BYTE = 0x20
|
||||
STREAM_BYTE_VALUES = 95
|
||||
LINE_BITS_PER_BYTE = 10 # 8N1: a start bit, 8 data bits, a stop bit
|
||||
|
||||
# SERTEST's DTR check (examples/sertest/sertest.c).
|
||||
DTR_CHECK = b"\x04"
|
||||
DTR_ACK = b"\x06"
|
||||
MARKS = (("off", b"JLDTR-OFF\r\n"), ("on", b"JLDTR-ON\r\n"), ("reopened", b"JLDTR-REOPENED\r\n"), ("hangup", b"JLDTR-HANGUP\r\n"))
|
||||
HANGUP_WATCH_SECONDS = 3.0
|
||||
|
||||
# DOSBox nullmodem transparent:0 framing.
|
||||
LINE_ESCAPE = 0xFF
|
||||
LINE_DTR = 0x02
|
||||
|
||||
|
||||
class LineDecoder:
|
||||
"""Splits a transparent:0 stream into data and the guest's DTR states,
|
||||
each state kept with the count of data bytes that came before it."""
|
||||
|
||||
def __init__(self):
|
||||
self.pending = b""
|
||||
self.dataCount = 0
|
||||
self.dtr = []
|
||||
|
||||
def feed(self, raw):
|
||||
buf = self.pending + raw
|
||||
data = bytearray()
|
||||
i = 0
|
||||
while i < len(buf):
|
||||
if buf[i] != LINE_ESCAPE:
|
||||
data.append(buf[i])
|
||||
i += 1
|
||||
continue
|
||||
if i + 1 >= len(buf):
|
||||
break
|
||||
if buf[i + 1] == LINE_ESCAPE:
|
||||
data.append(LINE_ESCAPE)
|
||||
else:
|
||||
self.dtr.append((self.dataCount + len(data), (buf[i + 1] & LINE_DTR) != 0))
|
||||
i += 2
|
||||
self.pending = buf[i:]
|
||||
self.dataCount += len(data)
|
||||
return bytes(data)
|
||||
|
||||
def dtrBefore(self, position):
|
||||
"""DTR as it was when the data byte at position arrived (None: never reported)."""
|
||||
state = None
|
||||
for at, dtr in self.dtr:
|
||||
if at > position:
|
||||
break
|
||||
state = dtr
|
||||
return state
|
||||
|
||||
def dtrChanges(self, first, last, value):
|
||||
"""Whether DTR was reported as value after data byte first, up to last."""
|
||||
return any(first < at <= last and dtr == value for at, dtr in self.dtr)
|
||||
|
||||
|
||||
def acceptOne(port, wall):
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind(("127.0.0.1", port))
|
||||
server.listen(1)
|
||||
server.settimeout(wall)
|
||||
try:
|
||||
conn, _ = server.accept()
|
||||
except socket.timeout:
|
||||
return None
|
||||
return conn
|
||||
|
||||
|
||||
def connectOne(port, wall, start):
|
||||
while time.time() - start < wall:
|
||||
try:
|
||||
return socket.create_connection(("127.0.0.1", port), timeout=2)
|
||||
except OSError:
|
||||
time.sleep(CONNECT_RETRY_SECONDS)
|
||||
return None
|
||||
|
||||
|
||||
def openPty(pathFile):
|
||||
master, slave = os.openpty()
|
||||
tty.setraw(slave)
|
||||
tty.setraw(master)
|
||||
# Publish atomically: the gate polls for the file.
|
||||
with open(pathFile + ".tmp", "w") as f:
|
||||
f.write(os.ttyname(slave))
|
||||
os.rename(pathFile + ".tmp", pathFile)
|
||||
return master
|
||||
|
||||
|
||||
def ackMarks(send, got, base, marks):
|
||||
# Acknowledge each of SERTEST's marker lines (after got[base:]) as it
|
||||
# completes, recording where it is in marks.
|
||||
while len(marks) < len(MARKS):
|
||||
name, mark = MARKS[len(marks)]
|
||||
at = got.find(mark, base)
|
||||
if at < 0:
|
||||
return
|
||||
marks[name] = at
|
||||
send(DTR_ACK)
|
||||
|
||||
|
||||
def closeHold(send, receive, wall, start, got, holdFile):
|
||||
# After the loopback: SERTEST's DTR check for its closes alone, then the
|
||||
# pty held open until holdFile exists.
|
||||
base = len(got)
|
||||
send(DTR_CHECK)
|
||||
marks = {}
|
||||
while len(marks) < len(MARKS) and time.time() - start < wall:
|
||||
data = receive()
|
||||
if data is None:
|
||||
break
|
||||
got += data
|
||||
ackMarks(send, got, base, marks)
|
||||
if len(marks) == len(MARKS):
|
||||
print("CLOSE PASS: every marker acknowledged; SERTEST has closed the port")
|
||||
status = 0
|
||||
else:
|
||||
print("CLOSE FAIL: no %r from SERTEST; stream=%r" % (MARKS[len(marks)][1], got[base:base + SHOWN_BYTES]))
|
||||
status = 1
|
||||
sys.stdout.flush()
|
||||
while not os.path.exists(holdFile) and time.time() - start < wall:
|
||||
receive()
|
||||
return status
|
||||
|
||||
|
||||
def dtrCheck(send, receive, wall, start, lines, got):
|
||||
# After the loopback: the stream so far is got; lines has decoded it.
|
||||
base = len(got)
|
||||
send(DTR_CHECK)
|
||||
marks = {}
|
||||
hangupAt = None
|
||||
while time.time() - start < wall:
|
||||
if hangupAt is not None and time.time() - hangupAt >= HANGUP_WATCH_SECONDS:
|
||||
break
|
||||
data = receive()
|
||||
if data is None:
|
||||
break
|
||||
got += data
|
||||
# The last marker starts the watch.
|
||||
ackMarks(send, got, base, marks)
|
||||
if hangupAt is None and len(marks) == len(MARKS):
|
||||
hangupAt = time.time()
|
||||
if len(marks) < len(MARKS):
|
||||
print("DTR FAIL: no %r from SERTEST; stream=%r" % (MARKS[len(marks)][1], got[base:base + SHOWN_BYTES]))
|
||||
return 1
|
||||
if time.time() - hangupAt < HANGUP_WATCH_SECONDS:
|
||||
print("DTR FAIL: out of time before the watch after the hang-up ended")
|
||||
return 1
|
||||
fails = []
|
||||
if lines.dtrBefore(marks["off"]) is not False:
|
||||
fails.append("jlSerialSetDtr(false) did not drop DTR")
|
||||
if lines.dtrBefore(marks["on"]) is not True:
|
||||
fails.append("jlSerialSetDtr(true) did not raise DTR")
|
||||
if lines.dtrChanges(marks["on"], marks["reopened"], False):
|
||||
fails.append("closing and opening the port dropped DTR")
|
||||
if lines.dtrBefore(marks["hangup"]) is not False:
|
||||
fails.append("jlSerialSetDtr(false) after the reopen did not drop DTR")
|
||||
if lines.dtrChanges(marks["hangup"], lines.dataCount, True):
|
||||
fails.append("closing the port after the hang-up raised DTR")
|
||||
if fails:
|
||||
print("DTR FAIL: %s; DTR states (data byte, DTR) since the check: %r" % ("; ".join(fails), [x for x in lines.dtr if x[0] >= base]))
|
||||
return 1
|
||||
print("DTR PASS: SetDtr(false/true) reached the line, close+open kept DTR up, a hang-up stayed down %.0f s after the close" % HANGUP_WATCH_SECONDS)
|
||||
return 0
|
||||
|
||||
|
||||
def loop(send, receive, wall, start, banner):
|
||||
# send(bytes); receive() -> bytes, b"" for "nothing yet", None at EOF.
|
||||
# Returns the exit status and the stream.
|
||||
got = b""
|
||||
sentAt = None
|
||||
lastSend = 0.0
|
||||
while time.time() - start < wall:
|
||||
if banner in got and time.time() - lastSend > RESEND_SECONDS:
|
||||
send(PROBE + b"\r\n")
|
||||
lastSend = time.time()
|
||||
if sentAt is None:
|
||||
sentAt = len(got)
|
||||
data = receive()
|
||||
if data is None:
|
||||
break
|
||||
got += data
|
||||
if sentAt is not None and PROBE in got[sentAt:]:
|
||||
print("LOOP PASS: banner %r seen, probe echoed; stream=%r" % (banner, got[:SHOWN_BYTES]))
|
||||
return 0, got
|
||||
print("LOOP FAIL: stream=%r" % got[:SHOWN_BYTES + 40])
|
||||
return 1, got
|
||||
|
||||
|
||||
def stream(send, receive, wall, start, total, rate):
|
||||
# The probe's last echoes go first, then the stream and its echo.
|
||||
pattern = bytes(STREAM_FIRST_BYTE + (k % STREAM_BYTE_VALUES) for k in range(total))
|
||||
settle = time.time() + STREAM_SETTLE_SECONDS
|
||||
while time.time() < settle:
|
||||
if receive() is None:
|
||||
print("STREAM FAIL: the connection closed before the stream")
|
||||
return 1
|
||||
sent = 0
|
||||
got = b""
|
||||
began = time.time()
|
||||
while len(got) < total and time.time() - start < wall:
|
||||
if sent < total and sent - len(got) < STREAM_WINDOW:
|
||||
chunk = pattern[sent:sent + STREAM_CHUNK]
|
||||
send(chunk)
|
||||
sent += len(chunk)
|
||||
data = receive()
|
||||
if data is None:
|
||||
break
|
||||
got += data
|
||||
elapsed = time.time() - began
|
||||
perSecond = len(got) / elapsed if elapsed > 0 else 0.0
|
||||
floor = rate / LINE_BITS_PER_BYTE * STREAM_MIN_PERCENT / 100.0
|
||||
if got != pattern:
|
||||
at = next((k for k in range(min(len(got), total)) if got[k] != pattern[k]), min(len(got), total))
|
||||
print("STREAM FAIL: %d of %d bytes back, the first wrong at %d; %r" % (len(got), total, at, got[at:at + 16]))
|
||||
return 1
|
||||
if perSecond < floor:
|
||||
print("STREAM FAIL: %d bytes back in order, %.0f a second, under %.0f (%d%% of %d baud)" % (total, perSecond, floor, STREAM_MIN_PERCENT, rate))
|
||||
return 1
|
||||
print("STREAM PASS: %d bytes back in order, none lost, %.0f a second (%d baud carries %d)" % (total, perSecond, rate, rate // LINE_BITS_PER_BYTE))
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv):
|
||||
withDtrCheck = "--dtr-check" in argv
|
||||
argv = [arg for arg in argv if arg != "--dtr-check"]
|
||||
streamBytes = 0
|
||||
rate = 0
|
||||
if "--stream" in argv and argv.index("--stream") + 1 < len(argv):
|
||||
at = argv.index("--stream")
|
||||
streamBytes = int(argv[at + 1])
|
||||
argv = argv[:at] + argv[at + 2:]
|
||||
if "--rate" in argv and argv.index("--rate") + 1 < len(argv):
|
||||
at = argv.index("--rate")
|
||||
rate = int(argv[at + 1])
|
||||
argv = argv[:at] + argv[at + 2:]
|
||||
holdFile = None
|
||||
if "--close-hold" in argv and argv.index("--close-hold") + 1 < len(argv):
|
||||
at = argv.index("--close-hold")
|
||||
holdFile = argv[at + 1]
|
||||
argv = argv[:at] + argv[at + 2:]
|
||||
if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty") or (holdFile is not None and argv[1] != "pty") or (streamBytes > 0 and (argv[1] == "pty" or withDtrCheck or rate <= 0)):
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
mode = argv[1]
|
||||
wall = float(argv[3])
|
||||
banner = argv[4].encode() if len(argv) > 4 else DEFAULT_BANNER
|
||||
start = time.time()
|
||||
|
||||
if mode == "pty":
|
||||
master = openPty(argv[2])
|
||||
|
||||
def receive():
|
||||
ready, _, _ = select.select([master], [], [], POLL_SECONDS)
|
||||
if not ready:
|
||||
return b""
|
||||
try:
|
||||
return os.read(master, READ_SIZE)
|
||||
except OSError:
|
||||
return b""
|
||||
|
||||
send = lambda data: os.write(master, data)
|
||||
status, got = loop(send, receive, wall, start, banner)
|
||||
if status != 0 or holdFile is None:
|
||||
return status
|
||||
return closeHold(send, receive, wall, start, got, holdFile)
|
||||
|
||||
port = int(argv[2])
|
||||
conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start)
|
||||
if conn is None:
|
||||
print("LOOP FAIL: no connection on port %d" % port)
|
||||
return 1
|
||||
conn.settimeout(POLL_SECONDS)
|
||||
|
||||
lines = LineDecoder()
|
||||
|
||||
def receive():
|
||||
try:
|
||||
data = conn.recv(READ_SIZE)
|
||||
except socket.timeout:
|
||||
return b""
|
||||
except OSError:
|
||||
return None
|
||||
if not data:
|
||||
return None
|
||||
return lines.feed(data) if withDtrCheck else data
|
||||
|
||||
status, got = loop(conn.sendall, receive, wall, start, banner)
|
||||
if status != 0:
|
||||
return status
|
||||
if streamBytes > 0:
|
||||
conn.settimeout(STREAM_POLL_SECONDS)
|
||||
return stream(conn.sendall, receive, wall, start, streamBytes, rate)
|
||||
if not withDtrCheck:
|
||||
return status
|
||||
return dtrCheck(conn.sendall, receive, wall, start, lines, got)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""serialStreamPeer.py - host end of a both-directions serial stream gate.
|
||||
|
||||
The guest (examples/sermouse) transmits an endless stream and checks an
|
||||
endless stream from us, both byte-exact:
|
||||
|
||||
byte k = (k ^ (k >> 8) ^ mask) & 0xFF
|
||||
|
||||
with mask GUEST_MASK for the guest's stream and PEER_MASK for ours (the same
|
||||
formula and masks as streamByte() in examples/sermouse/sermouse.c). A lost or
|
||||
repeated byte throws every byte after it off its expected value.
|
||||
|
||||
This peer listens (the emulator's null_modem connects out), checks the guest's
|
||||
stream in order, and answers it: our stream is paced byte for byte on the
|
||||
guest's, never more than WINDOW bytes ahead of what has come back. So when the
|
||||
guest pauses its stream the line goes quiet both ways, and once the guest has
|
||||
been silent for QUIET_SECONDS the counts are recorded as a quiet snapshot
|
||||
"sent:received" -- at that moment every byte either side sent has arrived, so a
|
||||
harness that reads the guest's own counts while it is paused can require them
|
||||
to equal one of these exactly.
|
||||
|
||||
serialStreamPeer.py PORT WALL MIN_BYTES
|
||||
|
||||
Runs until the emulator closes the link or WALL seconds pass, then prints one
|
||||
line, STREAM PASS or STREAM FAIL, with the counts and the quiet snapshots. PASS
|
||||
means the guest's stream arrived with no bad byte and at least MIN_BYTES long.
|
||||
Exit 0 on pass, 1 on fail.
|
||||
"""
|
||||
|
||||
import select
|
||||
import sys
|
||||
import time
|
||||
|
||||
from serialLoopPeer import acceptOne
|
||||
|
||||
GUEST_MASK = 0xA5
|
||||
PEER_MASK = 0x00
|
||||
WINDOW = 256 # our bytes allowed ahead of the guest's
|
||||
QUIET_SECONDS = 0.25 # guest silence that counts as a pause
|
||||
POLL_SECONDS = 0.05
|
||||
READ_SIZE = 4096
|
||||
BYTE_MASK = 0xFF
|
||||
BYTE_BITS = 8
|
||||
NO_BAD_INDEX = -1
|
||||
|
||||
|
||||
def streamByte(index, mask):
|
||||
return (index ^ (index >> BYTE_BITS) ^ mask) & BYTE_MASK
|
||||
|
||||
|
||||
def run(conn, wall, start):
|
||||
received = 0
|
||||
sent = 0
|
||||
bad = 0
|
||||
firstBad = NO_BAD_INDEX
|
||||
lastRx = 0.0
|
||||
quietArmed = False
|
||||
snapshots = []
|
||||
closed = False
|
||||
|
||||
conn.setblocking(False)
|
||||
while time.time() - start < wall:
|
||||
# Our stream starts once the guest's has: it is open and listening.
|
||||
want = received + WINDOW - sent if received > 0 else 0
|
||||
writers = [conn] if want > 0 else []
|
||||
readable, writable, _ = select.select([conn], writers, [], POLL_SECONDS)
|
||||
now = time.time()
|
||||
if readable:
|
||||
try:
|
||||
data = conn.recv(READ_SIZE)
|
||||
except OSError:
|
||||
data = b""
|
||||
if not data:
|
||||
closed = True
|
||||
break
|
||||
for value in data:
|
||||
if value != streamByte(received, GUEST_MASK):
|
||||
if firstBad == NO_BAD_INDEX:
|
||||
firstBad = received
|
||||
bad += 1
|
||||
received += 1
|
||||
lastRx = now
|
||||
quietArmed = True
|
||||
if writable and want > 0:
|
||||
chunk = bytes(streamByte(sent + i, PEER_MASK) for i in range(want))
|
||||
try:
|
||||
sent += conn.send(chunk)
|
||||
except OSError:
|
||||
pass
|
||||
if quietArmed and now - lastRx >= QUIET_SECONDS:
|
||||
snapshots.append("%d:%d" % (sent, received))
|
||||
quietArmed = False
|
||||
return received, sent, bad, firstBad, snapshots, closed
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 4:
|
||||
sys.stderr.write(__doc__)
|
||||
return 2
|
||||
port = int(argv[1])
|
||||
wall = float(argv[2])
|
||||
minBytes = int(argv[3])
|
||||
start = time.time()
|
||||
|
||||
print("STREAM listening on 127.0.0.1:%d" % port, flush=True)
|
||||
conn = acceptOne(port, wall)
|
||||
if conn is None:
|
||||
print("STREAM FAIL: the emulator never connected")
|
||||
return 1
|
||||
received, sent, bad, firstBad, snapshots, closed = run(conn, wall, start)
|
||||
conn.close()
|
||||
|
||||
ok = bad == 0 and received >= minBytes
|
||||
print("STREAM %s received=%d sent=%d bad=%d firstBad=%d quiet=%s end=%s" %
|
||||
("PASS" if ok else "FAIL", received, sent, bad, firstBad,
|
||||
",".join(snapshots) if snapshots else "none",
|
||||
"closed" if closed else "wall"))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Shared by the tile-bank gates, scripts/verify-<port>-tilebank.sh. Each runs
|
||||
# examples/tilebank on its port with the DATA/tilebank make/<port>.mk staged
|
||||
# (tests/host/tileBankStage.py: the port's own .tbk bake of
|
||||
# (tests/host/tileBankStage.sh: the port's own .tbk bake of
|
||||
# tests/host/tileBank.png, every other target's bake, the expected pixels),
|
||||
# gets the program's TILEBANK log line off the machine, and judges it here
|
||||
# against the host gate: scripts/check-tilebank.sh is run for its surface
|
||||
|
|
@ -102,9 +102,11 @@ tileBankNeedBuild() {
|
|||
# pass (its pixel checks are what make its hash worth matching), and set
|
||||
# TILEBANK_HASH the surface hash of its to-surface load of its own bake
|
||||
# TILEBANK_OTHERS how many targets besides any one port assetbake knows
|
||||
# (tests/host/tileBankStage.sh --targets)
|
||||
# Exits 2 without a host C compiler, 1 if the host gate fails.
|
||||
tileBankReference() {
|
||||
local out
|
||||
local targets
|
||||
|
||||
if ! command -v "${CC:-cc}" > /dev/null; then
|
||||
echo "$TILEBANK_GATE: no host C compiler (${CC:-cc}) for the reference run of scripts/check-tilebank.sh" >&2
|
||||
|
|
@ -120,11 +122,7 @@ tileBankReference() {
|
|||
echo "$TILEBANK_GATE: FAIL - scripts/check-tilebank.sh printed no to-surface hash" >&2
|
||||
exit 1
|
||||
fi
|
||||
TILEBANK_OTHERS="$(python3 - "$JOEY_REPO/tools/assetbake" <<'PY'
|
||||
import sys
|
||||
sys.path.insert(0, sys.argv[1])
|
||||
from assetbake import TARGET_NAMES
|
||||
print(len(TARGET_NAMES) - 1)
|
||||
PY
|
||||
)"
|
||||
# The stager asks assetbake (check-tilebank.sh has built it).
|
||||
targets="$("$JOEY_REPO/tests/host/tileBankStage.sh" --targets)" || exit 1
|
||||
TILEBANK_OTHERS=$(($(wc -w <<< "$targets") - 1))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""virtualPad.py - two virtual gamepads for the joystick gates of the ports
|
||||
that read the host's gamepads (SDL3, Win32 under Wine, the web port in
|
||||
Chromium).
|
||||
|
||||
Makes two Linux uinput devices laid out as Xbox 360 pads (the xpad driver's
|
||||
buttons and axes, Microsoft's USB ids), which SDL's, Wine's, and Chromium's
|
||||
gamepad mappings all know: stick A is the first pad, stick B the second.
|
||||
With --generic they are plain joysticks instead -- an X and a Y axis, a
|
||||
trigger and a thumb button, and pid.codes' test ids -- which no gamepad
|
||||
mapping knows, so SDL offers them only as raw joysticks. Then reads lines
|
||||
on stdin and holds or lets go of the controls named:
|
||||
|
||||
press CONTROL... CONTROL as joyGate.sh names them: A-left A-right
|
||||
release CONTROL... A-up A-down A-button0 A-button1, and the same for B
|
||||
quit remove the pads (as does end of input)
|
||||
|
||||
The left stick moves to the end of its travel (left/up -32767, right/down
|
||||
+32767; holding both of an axis's directions is the positive one), button
|
||||
0 is the south button (A) and button 1 the east (B) -- on a generic
|
||||
stick, the trigger and the thumb button. Prints "ready" once
|
||||
both devices exist, and "ok" after each line it has applied.
|
||||
|
||||
Needs write access to /dev/uinput (a desktop session's user usually has it
|
||||
through udev's uaccess rule). The pads are the whole machine's while they
|
||||
exist, not only the gate's.
|
||||
|
||||
virtualPad.py [--generic] [NAME] NAME prefixes the devices' names
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
UINPUT = "/dev/uinput"
|
||||
|
||||
# linux/uinput.h and linux/input.h.
|
||||
UI_DEV_CREATE = 0x5501
|
||||
UI_DEV_DESTROY = 0x5502
|
||||
UI_DEV_SETUP = 0x405C5503 # _IOW('U', 3, struct uinput_setup), 92 bytes
|
||||
UI_ABS_SETUP = 0x401C5504 # _IOW('U', 4, struct uinput_abs_setup), 28 bytes
|
||||
UI_SET_EVBIT = 0x40045564
|
||||
UI_SET_KEYBIT = 0x40045565
|
||||
UI_SET_ABSBIT = 0x40045567
|
||||
EV_SYN = 0x00
|
||||
EV_KEY = 0x01
|
||||
EV_ABS = 0x03
|
||||
SYN_REPORT = 0
|
||||
BUS_USB = 0x03
|
||||
XBOX360_VENDOR = 0x045E
|
||||
XBOX360_PRODUCT = 0x028E
|
||||
XBOX360_VERSION = 0x0110
|
||||
# pid.codes' vendor id and its test product, in no gamepad database.
|
||||
GENERIC_VENDOR = 0x1209
|
||||
GENERIC_PRODUCT = 0x0001
|
||||
GENERIC_VERSION = 0x0100
|
||||
NAME_BYTES = 80
|
||||
|
||||
BTN_TRIGGER = 0x120
|
||||
BTN_THUMB = 0x121
|
||||
BTN_SOUTH = 0x130
|
||||
BTN_EAST = 0x131
|
||||
BTN_NORTH = 0x133
|
||||
BTN_WEST = 0x134
|
||||
BTN_TL = 0x136
|
||||
BTN_TR = 0x137
|
||||
BTN_SELECT = 0x13A
|
||||
BTN_START = 0x13B
|
||||
BTN_MODE = 0x13C
|
||||
BTN_THUMBL = 0x13D
|
||||
BTN_THUMBR = 0x13E
|
||||
XBOX360_BUTTONS = (BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR,
|
||||
BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR)
|
||||
GENERIC_BUTTONS = (BTN_TRIGGER, BTN_THUMB)
|
||||
|
||||
ABS_X = 0x00
|
||||
ABS_Y = 0x01
|
||||
ABS_Z = 0x02
|
||||
ABS_RX = 0x03
|
||||
ABS_RY = 0x04
|
||||
ABS_RZ = 0x05
|
||||
ABS_HAT0X = 0x10
|
||||
ABS_HAT0Y = 0x11
|
||||
STICK_MAX = 32767
|
||||
STICK_MIN = -32768
|
||||
TRIGGER_MAX = 255
|
||||
# code: (min, max, fuzz, flat)
|
||||
STICK_AXES = {ABS_X: (STICK_MIN, STICK_MAX, 16, 128), ABS_Y: (STICK_MIN, STICK_MAX, 16, 128)}
|
||||
XBOX360_AXES = {
|
||||
**STICK_AXES,
|
||||
ABS_RX: (STICK_MIN, STICK_MAX, 16, 128), ABS_RY: (STICK_MIN, STICK_MAX, 16, 128),
|
||||
ABS_Z: (0, TRIGGER_MAX, 0, 0), ABS_RZ: (0, TRIGGER_MAX, 0, 0),
|
||||
ABS_HAT0X: (-1, 1, 0, 0), ABS_HAT0Y: (-1, 1, 0, 0),
|
||||
}
|
||||
# How long the kernel and the readers get to see a new device.
|
||||
SETTLE_SECONDS = 1.0
|
||||
FULL = STICK_MAX
|
||||
|
||||
|
||||
class Layout:
|
||||
def __init__(self, vendor, product, version, buttons, axes, line):
|
||||
self.vendor = vendor
|
||||
self.product = product
|
||||
self.version = version
|
||||
self.buttons = buttons
|
||||
self.axes = axes
|
||||
self.line = line
|
||||
|
||||
|
||||
XBOX360 = Layout(XBOX360_VENDOR, XBOX360_PRODUCT, XBOX360_VERSION, XBOX360_BUTTONS, XBOX360_AXES,
|
||||
{"button0": BTN_SOUTH, "button1": BTN_EAST})
|
||||
GENERIC = Layout(GENERIC_VENDOR, GENERIC_PRODUCT, GENERIC_VERSION, GENERIC_BUTTONS, STICK_AXES,
|
||||
{"button0": BTN_TRIGGER, "button1": BTN_THUMB})
|
||||
|
||||
|
||||
class Pad:
|
||||
def __init__(self, name, layout):
|
||||
self.fd = os.open(UINPUT, os.O_WRONLY | os.O_NONBLOCK)
|
||||
self.line = layout.line
|
||||
fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_KEY)
|
||||
for b in layout.buttons:
|
||||
fcntl.ioctl(self.fd, UI_SET_KEYBIT, b)
|
||||
fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_ABS)
|
||||
for code, (lo, hi, fuzz, flat) in layout.axes.items():
|
||||
fcntl.ioctl(self.fd, UI_SET_ABSBIT, code)
|
||||
fcntl.ioctl(self.fd, UI_ABS_SETUP, struct.pack("HH6i", code, 0, 0, lo, hi, fuzz, flat, 0))
|
||||
setup = struct.pack("HHHH%dsI" % NAME_BYTES, BUS_USB, layout.vendor, layout.product,
|
||||
layout.version, name.encode()[:NAME_BYTES - 1], 0)
|
||||
fcntl.ioctl(self.fd, UI_DEV_SETUP, setup)
|
||||
fcntl.ioctl(self.fd, UI_DEV_CREATE)
|
||||
self.held = set()
|
||||
|
||||
def emit(self, kind, code, value):
|
||||
now = time.time()
|
||||
os.write(self.fd, struct.pack("qqHHi", int(now), int((now % 1) * 1e6), kind, code, value))
|
||||
|
||||
def apply(self):
|
||||
"""Send the axes and buttons the held lines make, then a report."""
|
||||
x = FULL if "right" in self.held else (-FULL if "left" in self.held else 0)
|
||||
y = FULL if "down" in self.held else (-FULL if "up" in self.held else 0)
|
||||
self.emit(EV_ABS, ABS_X, x)
|
||||
self.emit(EV_ABS, ABS_Y, y)
|
||||
for line, code in self.line.items():
|
||||
self.emit(EV_KEY, code, 1 if line in self.held else 0)
|
||||
self.emit(EV_SYN, SYN_REPORT, 0)
|
||||
|
||||
def close(self):
|
||||
fcntl.ioctl(self.fd, UI_DEV_DESTROY)
|
||||
os.close(self.fd)
|
||||
|
||||
|
||||
def main(argv):
|
||||
args = argv[1:]
|
||||
layout = XBOX360
|
||||
if args and args[0] == "--generic":
|
||||
layout = GENERIC
|
||||
args = args[1:]
|
||||
prefix = args[0] if args else "JoeyLib gate"
|
||||
pads = {}
|
||||
try:
|
||||
for stick in ("A", "B"):
|
||||
pads[stick] = Pad("%s pad %s" % (prefix, stick), layout)
|
||||
time.sleep(SETTLE_SECONDS)
|
||||
for pad in pads.values():
|
||||
pad.apply()
|
||||
print("ready", flush=True)
|
||||
for raw in sys.stdin:
|
||||
words = raw.split()
|
||||
if not words:
|
||||
continue
|
||||
if words[0] == "quit":
|
||||
break
|
||||
if words[0] not in ("press", "release"):
|
||||
print("error: unknown command %s" % words[0], flush=True)
|
||||
continue
|
||||
touched = set()
|
||||
for control in words[1:]:
|
||||
stick, _, line = control.partition("-")
|
||||
if stick not in pads or line not in ("left", "right", "up", "down", "button0", "button1"):
|
||||
print("error: unknown control %s" % control, flush=True)
|
||||
continue
|
||||
if words[0] == "press":
|
||||
pads[stick].held.add(line)
|
||||
else:
|
||||
pads[stick].held.discard(line)
|
||||
touched.add(stick)
|
||||
for stick in touched:
|
||||
pads[stick].apply()
|
||||
print("ok", flush=True)
|
||||
finally:
|
||||
for pad in pads.values():
|
||||
pad.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -43,9 +43,11 @@ webNeed() {
|
|||
|
||||
webServe() {
|
||||
local tries
|
||||
local server
|
||||
server=$(joeyTool httpServe) || exit 1
|
||||
for ((tries = 0; tries < 10; tries++)); do
|
||||
WEB_PORT=$((WEB_PORT_MIN + RANDOM % WEB_PORT_SPAN))
|
||||
python3 -m http.server "$WEB_PORT" --bind 127.0.0.1 --directory "$1" > /dev/null 2>&1 &
|
||||
"$server" "$WEB_PORT" "$1" > /dev/null 2>&1 &
|
||||
WEB_SERVER_PID=$!
|
||||
sleep 0.5
|
||||
if joeyAlive "$WEB_SERVER_PID"; then
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ win16Stage() {
|
|||
cp -r "$WIN16_C" "$work/c"
|
||||
cp "$KPJOY_DRV" "$work/c/WINDOWS/SYSTEM/KPJOY.DRV"
|
||||
cp "$KPCOMM_DRV" "$work/c/WINDOWS/SYSTEM/KPCOMM.DRV"
|
||||
python3 "$JOEY_REPO/tools/winIni.py" "$work/c/WINDOWS/SYSTEM.INI" \
|
||||
"$(joeyTool winIni)" "$work/c/WINDOWS/SYSTEM.INI" \
|
||||
set boot shell "D:\\$3" \
|
||||
set boot comm.drv kpcomm.drv \
|
||||
set drivers joystick kpjoy.drv
|
||||
|
|
@ -107,13 +107,18 @@ WIN16_WING_PROFILE="ECHGECHGECHGECHGDDFFDDFFDDFFDDFFZZZZZZZZZZZZZZZZDDFGDGFGDDFF
|
|||
win16StageWinG() {
|
||||
local file
|
||||
local dosDate
|
||||
local year
|
||||
local month
|
||||
local day
|
||||
for file in WING.DLL WINGDE.DLL WINGDIB.DRV WINGPAL.WND DVA.386; do
|
||||
cp "$WING_DIR/$file" "$1/c/WINDOWS/SYSTEM/$file"
|
||||
done
|
||||
dosDate=$(python3 -c 'import os, sys, time; t = time.localtime(os.path.getmtime(sys.argv[1])); print("%04X" % (((t.tm_year - 1980) << 9) | (t.tm_mon << 5) | t.tm_mday))' "$1/c/WINDOWS/SYSTEM/$WIN16_WING_DRIVER")
|
||||
python3 "$JOEY_REPO/tools/winIni.py" "$1/c/WINDOWS/SYSTEM.INI" \
|
||||
# The driver's date as a DOS date word: years from 1980, month, day.
|
||||
read -r year month day <<< "$(date -r "$1/c/WINDOWS/SYSTEM/$WIN16_WING_DRIVER" '+%Y %m %d')"
|
||||
dosDate=$(printf '%04X' $((((year - 1980) << 9) | (10#$month << 5) | 10#$day)))
|
||||
"$(joeyTool winIni)" "$1/c/WINDOWS/SYSTEM.INI" \
|
||||
add 386Enh device "C:\\WINDOWS\\SYSTEM\\DVA.386"
|
||||
python3 "$JOEY_REPO/tools/winIni.py" "$1/c/WINDOWS/WIN.INI" \
|
||||
"$(joeyTool winIni)" "$1/c/WINDOWS/WIN.INI" \
|
||||
set WinG "$WIN16_WING_PROFILE_HEAD-$dosDate-$WIN16_WING_PROFILE_TAIL" "$WIN16_WING_PROFILE"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,8 +69,12 @@ x68kDataDisk() {
|
|||
}
|
||||
|
||||
|
||||
# x68kXdf ARGS... -- tools/xdftool.py (list, add, delete, deltree, empty,
|
||||
# extract, free).
|
||||
# x68kXdf ARGS... -- tools/xdftool (list, add, delete, deltree, empty,
|
||||
# extract, free). make/tools.mk builds it the first time a shell calls this
|
||||
# (staging a DATA tree calls it once a file).
|
||||
x68kXdf() {
|
||||
python3 "$JOEY_REPO/tools/xdftool.py" "$@"
|
||||
if [ -z "${X68K_XDFTOOL:-}" ]; then
|
||||
X68K_XDFTOOL="$(joeyTool xdftool)" || return 1
|
||||
fi
|
||||
"$X68K_XDFTOOL" "$@"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,93 +142,4 @@ if [ -n "${PROFILE_SAMPLES:-}" ]; then
|
|||
cp "$work/samples.txt" "$PROFILE_SAMPLES"
|
||||
fi
|
||||
|
||||
python3 - "$work/samples.txt" "$map" "$top" <<'PY'
|
||||
import bisect
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
samplesPath, mapPath, top = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
||||
mapBase = mapPath[:-4] if mapPath.endswith('.map') else mapPath
|
||||
|
||||
# The segments: number -> (link bank, image bytes).
|
||||
segments = {}
|
||||
manifest = mapBase + '.json'
|
||||
if os.path.exists(manifest):
|
||||
for seg in json.load(open(manifest))['segments']:
|
||||
segments[seg['num']] = int(seg['base'], 16) >> 16
|
||||
else:
|
||||
segments[1] = 0
|
||||
images = {num: open('%s.seg%d.bin' % (mapBase, num), 'rb').read() for num in segments}
|
||||
|
||||
# The symbols, by address: the globals, and the locals (static functions)
|
||||
# when the map has them; a global wins an address both name.
|
||||
byAddr = {}
|
||||
section = None
|
||||
for line in open(mapPath):
|
||||
if line.startswith('#'):
|
||||
section = 'global' if line.startswith('# global symbols') else ('local' if line.startswith('# local symbols') else None)
|
||||
continue
|
||||
m = re.match(r'^0x([0-9a-fA-F]{6})\s+(\S+)$', line.strip())
|
||||
if m and section is not None:
|
||||
addr = int(m.group(1), 16)
|
||||
if section == 'global' or addr not in byAddr:
|
||||
byAddr[addr] = m.group(2)
|
||||
syms = sorted(byAddr.items())
|
||||
symAddrs = [a for a, _ in syms]
|
||||
|
||||
counts = {}
|
||||
fingerprints = {}
|
||||
total = 0
|
||||
for line in open(samplesPath):
|
||||
parts = line.split()
|
||||
if parts[0] == 'S':
|
||||
counts[int(parts[1], 16)] = int(parts[2])
|
||||
elif parts[0] == 'F':
|
||||
fingerprints[int(parts[1], 16)] = bytes.fromhex(parts[2])
|
||||
elif parts[0] == 'T':
|
||||
total = int(parts[1])
|
||||
|
||||
# Runtime bank -> link bank, by the best-scoring segment image.
|
||||
bankMap = {}
|
||||
for bank, fp in fingerprints.items():
|
||||
best = None
|
||||
for num, image in images.items():
|
||||
n = min(len(fp), len(image))
|
||||
if n < 16:
|
||||
continue
|
||||
same = sum(1 for i in range(n) if fp[i] == image[i])
|
||||
score = same / n
|
||||
if best is None or score > best[0]:
|
||||
best = (score, num)
|
||||
if best is not None and best[0] >= 0.85:
|
||||
bankMap[bank] = (segments[best[1]], best[1], best[0])
|
||||
|
||||
def where(addr):
|
||||
bank = addr >> 16
|
||||
if bank in bankMap:
|
||||
link = (bankMap[bank][0] << 16) | (addr & 0xFFFF)
|
||||
i = bisect.bisect_right(symAddrs, link) - 1
|
||||
if i >= 0 and (symAddrs[i] >> 16) == (link >> 16):
|
||||
return syms[i][1]
|
||||
return 'seg%d+%04X' % (bankMap[bank][1], addr & 0xFFFF)
|
||||
if bank >= 0xFC:
|
||||
return 'ROM $%02X' % bank
|
||||
if bank in (0xE0, 0xE1):
|
||||
return 'bank $%02X (firmware/vectors)' % bank
|
||||
return 'bank $%02X (GS/OS, tools, NTP)' % bank
|
||||
|
||||
byName = {}
|
||||
for addr, n in counts.items():
|
||||
key = where(addr)
|
||||
byName[key] = byName.get(key, 0) + n
|
||||
|
||||
print('profile-iigs: %d samples' % total)
|
||||
for bank in sorted(bankMap):
|
||||
link, num, score = bankMap[bank]
|
||||
print(' bank $%02X = segment %d (link bank $%02X, %.0f%% of %d bytes match)' % (bank, num, link, score * 100, len(fingerprints[bank])))
|
||||
print('%7s %6s %s' % ('samples', 'pct', 'where'))
|
||||
for key, n in sorted(byName.items(), key=lambda kv: -kv[1])[:top]:
|
||||
print('%7d %5.1f%% %s' % (n, 100.0 * n / max(total, 1), key))
|
||||
PY
|
||||
"$(joeyTool iigsProfile)" "$work/samples.txt" "$map" "$top"
|
||||
|
|
|
|||
|
|
@ -63,12 +63,7 @@ textSize=$(m68k-xelf-readelf -S "$elf" | awk '/\.text/ { print strtonum("0x" $7)
|
|||
# The .X header is 64 bytes, so the file offset is 64 + the .text offset.
|
||||
relocSite=$(m68k-xelf-readelf -r "$elf" | awk '$2 ~ /01$/ && $3 == "R_68K_32" { print strtonum("0x" $1); exit }')
|
||||
[[ -n ${relocSite:-} ]] || { echo "profile-x68000: no R_68K_32 relocation to fingerprint with" >&2; exit 2; }
|
||||
relocValue=$(python3 -c "
|
||||
import sys
|
||||
d = open('$bin','rb').read()
|
||||
o = 64 + $relocSite
|
||||
print(int.from_bytes(d[o:o+4],'big'))
|
||||
")
|
||||
relocValue=$(od -An -tu4 -N4 --endian=big -j $((64 + relocSite)) "$bin" | tr -d ' ')
|
||||
|
||||
# jlgMouseX is written once per input poll, i.e. once per game frame.
|
||||
mouseXOff=$(m68k-xelf-nm "$elf" | awk '$3 == "jlgMouseX" { print strtonum("0x" $1); exit }')
|
||||
|
|
@ -255,73 +250,113 @@ if [[ -n ${PROFILE_SAMPLES:-} ]]; then
|
|||
fi
|
||||
|
||||
m68k-xelf-nm -n "$elf" > "$work/syms.txt"
|
||||
python3 - "$work/prof.txt" "$work/syms.txt" <<'PY'
|
||||
import sys
|
||||
prof, symfile = sys.argv[1], sys.argv[2]
|
||||
syms = []
|
||||
for line in open(symfile):
|
||||
p = line.split()
|
||||
if len(p) == 3 and p[1] in ('t', 'T'):
|
||||
syms.append((int(p[0], 16), p[2]))
|
||||
syms.sort()
|
||||
|
||||
def owner(off):
|
||||
lo, hi = 0, len(syms) - 1
|
||||
best = None
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
if syms[mid][0] <= off:
|
||||
best = syms[mid][1]
|
||||
LC_ALL=C awk '
|
||||
# Most first; equal ones in the order first seen.
|
||||
function byCount(i1, v1, i2, v2) {
|
||||
if (count[v1] != count[v2]) {
|
||||
return count[v2] - count[v1]
|
||||
}
|
||||
return first[v1] - first[v2]
|
||||
}
|
||||
# The last symbol at or below an offset (the highest address; of equal
|
||||
# ones, the last name in order), as its index; 0 for none.
|
||||
function owner(off, lo, hi, mid, best) {
|
||||
lo = 1
|
||||
hi = symCount
|
||||
best = 0
|
||||
while (lo <= hi) {
|
||||
mid = int((lo + hi) / 2)
|
||||
if (symAddr[mid] <= off) {
|
||||
best = mid
|
||||
lo = mid + 1
|
||||
else:
|
||||
} else {
|
||||
hi = mid - 1
|
||||
return best or "?"
|
||||
|
||||
total = 0
|
||||
fps = None
|
||||
hits = {}
|
||||
buckets = {}
|
||||
outside = 0
|
||||
for line in open(prof):
|
||||
p = line.split()
|
||||
if p and p[0] == 'total':
|
||||
total = int(p[1])
|
||||
elif len(p) == 2 and p[0].lstrip('-').isdigit():
|
||||
off, n = int(p[0]), int(p[1])
|
||||
buckets[off] = buckets.get(off, 0) + n
|
||||
if off < 0:
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
function bySymbol(i1, v1, i2, v2) {
|
||||
if (symAddr[v1] != symAddr[v2]) {
|
||||
return symAddr[v1] - symAddr[v2]
|
||||
}
|
||||
return symName[v1] < symName[v2] ? -1 : (symName[v1] > symName[v2])
|
||||
}
|
||||
FNR == NR {
|
||||
if (NF == 3 && ($2 == "t" || $2 == "T")) {
|
||||
rawCount++
|
||||
rawAddr[rawCount] = strtonum("0x" $1)
|
||||
rawName[rawCount] = $3
|
||||
}
|
||||
next
|
||||
}
|
||||
FNR == 1 {
|
||||
for (i = 1; i <= rawCount; i++) {
|
||||
symAddr[i] = rawAddr[i]
|
||||
symName[i] = rawName[i]
|
||||
order[i] = i
|
||||
}
|
||||
symCount = rawCount
|
||||
asort(order, sorted, "bySymbol")
|
||||
for (i = 1; i <= symCount; i++) {
|
||||
addr[i] = symAddr[sorted[i]]
|
||||
name[i] = symName[sorted[i]]
|
||||
}
|
||||
for (i = 1; i <= symCount; i++) {
|
||||
symAddr[i] = addr[i]
|
||||
symName[i] = name[i]
|
||||
}
|
||||
}
|
||||
$1 == "total" {
|
||||
total = $2 + 0
|
||||
next
|
||||
}
|
||||
NF == 2 && $1 ~ /^-*[0-9]+$/ {
|
||||
off = $1 + 0
|
||||
n = $2 + 0
|
||||
key = "b" off
|
||||
if (!(key in count)) {
|
||||
first[key] = ++seen
|
||||
buckets[++bucketCount] = key
|
||||
bucketOff[key] = off
|
||||
}
|
||||
count[key] += n
|
||||
if (off < 0) {
|
||||
outside += n
|
||||
else:
|
||||
hits[owner(off)] = hits.get(owner(off), 0) + n
|
||||
if total == 0:
|
||||
sys.exit("profile-x68000: no samples recorded")
|
||||
rows = sorted(hits.items(), key=lambda kv: -kv[1])
|
||||
fpsText = f", game running at {fps:.1f} fps" if fps is not None and fps > 0.0 else " (frame count unavailable)"
|
||||
print(f"profile-x68000: {total} samples, {outside} ({outside*100.0/total:.1f}%) outside the program (ROM/IOCS){fpsText}")
|
||||
shown = 0
|
||||
for name, n in rows[:25]:
|
||||
print(f" {n*100.0/total:5.1f}% {n:6d} {name}")
|
||||
shown += n
|
||||
print(f" {(total-outside-shown)*100.0/total:5.1f}% {total-outside-shown:6d} (everything else)")
|
||||
|
||||
# The hottest 64-byte buckets, named as symbol+offset. Function totals say
|
||||
# which routine to look at; these say which LINE of it, which is the
|
||||
# difference between rewriting a loop and rewriting the wrong loop.
|
||||
print("\nhottest addresses (symbol + offset into it):")
|
||||
def ownerOff(off):
|
||||
lo, hi = 0, len(syms) - 1
|
||||
best = None
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
if syms[mid][0] <= off:
|
||||
best = syms[mid]
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
return (best[1], off - best[0]) if best else ("?", 0)
|
||||
for off, n in sorted(buckets.items(), key=lambda kv: -kv[1])[:20]:
|
||||
if off < 0:
|
||||
next
|
||||
}
|
||||
s = owner(off)
|
||||
key = "s" (s > 0 ? symName[s] : "?")
|
||||
if (!(key in count)) {
|
||||
first[key] = ++seen
|
||||
hits[++hitCount] = key
|
||||
}
|
||||
count[key] += n
|
||||
}
|
||||
END {
|
||||
if (total == 0) {
|
||||
print "profile-x68000: no samples recorded" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
printf "profile-x68000: %d samples, %d (%.1f%%) outside the program (ROM/IOCS) (frame count unavailable)\n", total, outside, outside * 100.0 / total
|
||||
asort(hits, ranked, "byCount")
|
||||
shown = 0
|
||||
for (i = 1; i <= hitCount && i <= 25; i++) {
|
||||
printf " %5.1f%% %6d %s\n", count[ranked[i]] * 100.0 / total, count[ranked[i]], substr(ranked[i], 2)
|
||||
shown += count[ranked[i]]
|
||||
}
|
||||
printf " %5.1f%% %6d (everything else)\n", (total - outside - shown) * 100.0 / total, total - outside - shown
|
||||
# The hottest 64-byte buckets, named as symbol+offset. Function
|
||||
# totals say which routine to look at; these say which LINE of it,
|
||||
# which is the difference between rewriting a loop and rewriting
|
||||
# the wrong loop.
|
||||
print "\nhottest addresses (symbol + offset into it):"
|
||||
asort(buckets, ranked, "byCount")
|
||||
for (i = 1; i <= bucketCount && i <= 20; i++) {
|
||||
off = bucketOff[ranked[i]]
|
||||
if (off < 0) {
|
||||
continue
|
||||
name, delta = ownerOff(off)
|
||||
print(f" {n*100.0/total:5.1f}% {n:6d} {name}+0x{delta:x}")
|
||||
PY
|
||||
}
|
||||
s = owner(off)
|
||||
printf " %5.1f%% %6d %s+0x%x\n", count[ranked[i]] * 100.0 / total, count[ranked[i]], (s > 0 ? symName[s] : "?"), (s > 0 ? off - symAddr[s] : 0)
|
||||
}
|
||||
}' "$work/syms.txt" "$work/prof.txt"
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@
|
|||
# dos, dvx, win16 DOSBox's 4-axis game port from host keys (and no
|
||||
# game port: no stick)
|
||||
# sdl3, win32 two virtual uinput gamepads (scripts/lib/
|
||||
# virtualPad.py; needs /dev/uinput), then unplugged;
|
||||
# virtualPad; needs /dev/uinput), then unplugged;
|
||||
# sdl3 again with two joysticks SDL cannot map
|
||||
# web the page's Gamepad API, stood in for by the driver
|
||||
# psp: no keyboard; its d-pad, nub, cross and circle as joystick
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
# timeline of tones, voice and noise steps, effects, pans, a
|
||||
# one-shot module and ticks; its sound is recorded and graded
|
||||
# against what it logged (scripts/verify-audio-conformance.sh,
|
||||
# tools/gradeAudioConformance.py): pitch, level per attenuation
|
||||
# tests/gate/audioGate.c): pitch, level per attenuation
|
||||
# step, noise rate, mix balance, centring and pan where the port
|
||||
# is stereo, the module's end, the tick rate. Every port that
|
||||
# records sound: all but beos, whose VM has no sound card
|
||||
|
|
@ -226,6 +226,9 @@ KINDS="host golden visual tilebank input audio conform serial mouse save loss di
|
|||
ALL_PORTS="host iigs x68000 atarist ste amiga dos dvx sdl3 web macos win32 win16 psp haiku haiku32 beos"
|
||||
# UBER on the Amiga needs longer than bench-amiga.sh's interactive default.
|
||||
AMIGA_UBER_SECONDS=400
|
||||
# UBER's per-op hashes against a golden: make/tools.mk's diffUberHashes, built
|
||||
# first when it is not current.
|
||||
UBER_HASHES="make -s -f make/tools.mk diffUberHashes > /dev/null && \"${JOEY_BUILD:-build}/tools/diffUberHashes\""
|
||||
|
||||
# kind port name command...: every gate, in run order within its kind.
|
||||
GATES=$(cat <<EOF
|
||||
|
|
@ -244,26 +247,26 @@ host host tilebank scripts/check-tilebank.sh
|
|||
host host layer scripts/check-tilelayer.sh
|
||||
host host tiled scripts/check-tiled.sh
|
||||
host host vgm scripts/check-vgm.sh
|
||||
golden iigs uber scripts/bench-iigs.sh && tools/diff-uber-hashes tests/goldens/uber/iigs.txt build/iigs/bin/joeylog.txt
|
||||
golden iigs uber scripts/bench-iigs.sh && $UBER_HASHES tests/goldens/uber/iigs.txt build/iigs/bin/joeylog.txt
|
||||
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 atarist uber scripts/bench-atarist.sh && $UBER_HASHES tests/goldens/uber/atarist.txt build/atarist/bin/joeylog.txt
|
||||
golden atarist marks scripts/verify-atarist-dirtymark.sh
|
||||
golden ste uber scripts/bench-atarist.sh ste && tools/diff-uber-hashes tests/goldens/uber/atarist.txt build/ste/bin/joeylog-ste.txt
|
||||
golden ste megaste scripts/bench-atarist.sh megaste && tools/diff-uber-hashes tests/goldens/uber/atarist.txt build/ste/bin/joeylog-megaste.txt
|
||||
golden ste uber scripts/bench-atarist.sh ste && $UBER_HASHES tests/goldens/uber/atarist.txt build/ste/bin/joeylog-ste.txt
|
||||
golden ste megaste scripts/bench-atarist.sh megaste && $UBER_HASHES tests/goldens/uber/atarist.txt build/ste/bin/joeylog-megaste.txt
|
||||
golden ste machine scripts/verify-ste-machine.sh
|
||||
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 amiga a1200 JOEY_AMIGA_MODEL=A1200 TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && tools/diff-uber-hashes tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt
|
||||
golden amiga a4000 JOEY_AMIGA_MODEL=A4000 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
|
||||
golden sdl3 uber scripts/bench-sdl3.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
|
||||
golden web uber scripts/bench-web.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
|
||||
golden win32 uber scripts/bench-win32.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/win32/bin/joeylog.txt
|
||||
golden win16 uber scripts/bench-win16.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/win16/bin/JOEYLOG.TXT
|
||||
golden psp uber scripts/bench-psp.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/psp/bin/UBER/joeylog.txt
|
||||
golden haiku uber scripts/bench-be.sh haiku && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/haiku/bin/joeylog.txt
|
||||
golden haiku32 uber scripts/bench-be.sh haiku32 && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/haiku32/bin/joeylog.txt
|
||||
golden beos uber scripts/bench-be.sh beos && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/beos/bin/joeylog.txt
|
||||
golden amiga uber TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && $UBER_HASHES tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt
|
||||
golden amiga a1200 JOEY_AMIGA_MODEL=A1200 TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && $UBER_HASHES tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt
|
||||
golden amiga a4000 JOEY_AMIGA_MODEL=A4000 TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && $UBER_HASHES tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt
|
||||
golden dos uber scripts/bench-dos.sh && $UBER_HASHES tests/goldens/uber/dos.txt build/dos/bin/JOEYLOG.TXT
|
||||
golden dvx uber scripts/bench-dvx.sh && $UBER_HASHES tests/goldens/uber/dos.txt build/dvx/bin/joeylog.txt
|
||||
golden sdl3 uber scripts/bench-sdl3.sh && $UBER_HASHES tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
|
||||
golden web uber scripts/bench-web.sh && $UBER_HASHES tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
|
||||
golden win32 uber scripts/bench-win32.sh && $UBER_HASHES tests/goldens/uber/dos.txt build/win32/bin/joeylog.txt
|
||||
golden win16 uber scripts/bench-win16.sh && $UBER_HASHES tests/goldens/uber/dos.txt build/win16/bin/JOEYLOG.TXT
|
||||
golden psp uber scripts/bench-psp.sh && $UBER_HASHES tests/goldens/uber/dos.txt build/psp/bin/UBER/joeylog.txt
|
||||
golden haiku uber scripts/bench-be.sh haiku && $UBER_HASHES tests/goldens/uber/dos.txt build/haiku/bin/joeylog.txt
|
||||
golden haiku32 uber scripts/bench-be.sh haiku32 && $UBER_HASHES tests/goldens/uber/dos.txt build/haiku32/bin/joeylog.txt
|
||||
golden beos uber scripts/bench-be.sh beos && $UBER_HASHES tests/goldens/uber/dos.txt build/beos/bin/joeylog.txt
|
||||
visual iigs examples scripts/verify-iigs-all.sh
|
||||
visual iigs display make -s -f make/iigs.mk iigs-verify-shrtail || exit 1
|
||||
visual dvx full scripts/verify-dvx-fullscreen.sh
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
# example binary name in build/amiga/bin, default Audio
|
||||
# Env: AMIGA_AUDIO_KEEP set to keep the work dir and the WAV
|
||||
# Requires: fs-uae, Xvfb, xdotool, parec/pactl (PulseAudio or PipeWire),
|
||||
# python3, the Kickstart (install.sh install emulator:amiga),
|
||||
# the Kickstart (install.sh install emulator:amiga),
|
||||
# build/amiga/bin/<example>
|
||||
set -uo pipefail
|
||||
|
||||
|
|
@ -55,15 +55,14 @@ source "$repo/scripts/lib/amigaGate.sh"
|
|||
amigaMachine
|
||||
prog=${1:-Audio}
|
||||
binDir=$repo/build/amiga/bin
|
||||
grader=$repo/tools/gradeAudioPhases.py
|
||||
grader=$(joeyTool audioGate) || exit 1
|
||||
pulseLib=$repo/scripts/pulseRecordLib.sh
|
||||
label=verify-amiga-audio
|
||||
|
||||
[ -f "$grader" ] || { echo "$label: missing $grader" >&2; exit 2; }
|
||||
[ -f "$pulseLib" ] || { echo "$label: missing $pulseLib" >&2; exit 2; }
|
||||
# shellcheck source=scripts/pulseRecordLib.sh
|
||||
. "$pulseLib"
|
||||
for tool in Xvfb xdotool parec pactl python3; do
|
||||
for tool in Xvfb xdotool parec pactl; 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 (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; }
|
||||
|
|
@ -178,7 +177,7 @@ sleep 1
|
|||
|
||||
# Thresholds sit far from the measured values (music ~5700, paused ~240,
|
||||
# sfx ~1170) so ordinary mixing jitter cannot flip the verdict.
|
||||
python3 "$grader" "$label" "$work/out.wav" \
|
||||
"$grader" phases "$label" "$work/out.wav" \
|
||||
"music:1:5:2000:" \
|
||||
"paused:8:13::800" \
|
||||
"sfx:16:22:800:" \
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
# verify-amiga-icon.sh - a program's Workbench icon: read and drawn by the
|
||||
# Amiga's own icon.library, and the program started from it.
|
||||
#
|
||||
# make bakes Name.info beside every Amiga program (tools/iconBake.py info:
|
||||
# make bakes Name.info beside every Amiga program (tools/iconBake info:
|
||||
# a tool's icon in Workbench's four colours, with the stack it starts
|
||||
# with). WBLAUNCH (examples/wblaunch) runs under FS-UAE on an A500 with
|
||||
# PALPROBE and its icon in a folder of their own, Prog/. It reads the icon
|
||||
|
|
@ -91,99 +91,4 @@ if ! waitFor "WBLAUNCH: returned"; then
|
|||
fi
|
||||
cat "$log"
|
||||
|
||||
python3 - "$work" "$want_stack" "$WBTOOL" "$label" <<'PY'
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
work, wantStack, wbTool, label = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4]
|
||||
fails = []
|
||||
log = open(os.path.join(work, "wblaunch.txt"), encoding="ascii", errors="replace").read()
|
||||
for line in log.splitlines():
|
||||
if "FAIL" in line:
|
||||
fails.append(line)
|
||||
|
||||
# What the file holds: its image's size, depth and planes (after the 78-byte
|
||||
# DiskObject and the 20-byte struct Image).
|
||||
info = open(os.path.join(work, "Prog", "Palprobe.info"), "rb").read()
|
||||
width, height, depth = struct.unpack(">hhh", info[82:88])
|
||||
rowBytes = (width + 15) // 16 * 2
|
||||
planeBytes = rowBytes * height
|
||||
indices = [[0] * width for _ in range(height)]
|
||||
for plane in range(depth):
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
byte = info[98 + plane * planeBytes + y * rowBytes + x // 8]
|
||||
if byte & (0x80 >> (x % 8)):
|
||||
indices[y][x] |= 1 << plane
|
||||
|
||||
m = re.search(r"icon type=(\d+) stack=(\d+) gadget=(\d+)x(\d+) image=(\d+)x(\d+)x(\d+)", log)
|
||||
if m is None:
|
||||
fails.append("icon.library never read the icon")
|
||||
else:
|
||||
got = tuple(int(v) for v in m.groups())
|
||||
if got[0] != wbTool:
|
||||
fails.append(f"icon type {got[0]}, want WBTOOL ({wbTool})")
|
||||
if got[1] != wantStack:
|
||||
fails.append(f"icon stack {got[1]}, want {wantStack}")
|
||||
if got[4:] != (width, height, depth):
|
||||
fails.append(f"icon.library's image {got[4]}x{got[5]}x{got[6]}, the file's {width}x{height}x{depth}")
|
||||
|
||||
# Where the icon is on the screenshot: the screen is Workbench's grey, and
|
||||
# the icon's coloured pixels lie inside it. The file's coloured pixels
|
||||
# (index not 0) and the screenshot's (not grey) must have the same bounds,
|
||||
# scaled; then each of the file's pixels is looked at in the middle of its
|
||||
# cell.
|
||||
colours = [(170, 170, 170), (0, 0, 0), (255, 255, 255), (102, 136, 187)]
|
||||
shot = Image.open(os.path.join(work, "screen.png")).convert("RGB")
|
||||
w, h = shot.size
|
||||
pixels = shot.load()
|
||||
grey = [(x, y) for y in range(0, h, 2) for x in range(0, w, 2) if pixels[x, y] == colours[0]]
|
||||
if not grey:
|
||||
fails.append("no Workbench grey on the screenshot")
|
||||
else:
|
||||
gx0 = min(p[0] for p in grey)
|
||||
gx1 = max(p[0] for p in grey)
|
||||
gy0 = min(p[1] for p in grey)
|
||||
# The upper half: FS-UAE prints its notices over the bottom.
|
||||
gy1 = (gy0 + max(p[1] for p in grey)) // 2
|
||||
shown = [(x, y) for y in range(gy0, gy1 + 1) for x in range(gx0, gx1 + 1) if pixels[x, y] != colours[0]]
|
||||
held = [(x, y) for y in range(height) for x in range(width) if indices[y][x] != 0]
|
||||
if not shown or not held:
|
||||
fails.append("no icon on the screenshot" if held else "the icon file is empty")
|
||||
else:
|
||||
sx0 = min(p[0] for p in shown)
|
||||
sx1 = max(p[0] for p in shown)
|
||||
sy0 = min(p[1] for p in shown)
|
||||
sy1 = max(p[1] for p in shown)
|
||||
fx0 = min(p[0] for p in held)
|
||||
fx1 = max(p[0] for p in held)
|
||||
fy0 = min(p[1] for p in held)
|
||||
fy1 = max(p[1] for p in held)
|
||||
scaleX = (sx1 + 1 - sx0) / (fx1 + 1 - fx0)
|
||||
scaleY = (sy1 + 1 - sy0) / (fy1 + 1 - fy0)
|
||||
wrong = 0
|
||||
for y in range(fy0, fy1 + 1):
|
||||
for x in range(fx0, fx1 + 1):
|
||||
at = (int(sx0 + (x - fx0 + 0.5) * scaleX), int(sy0 + (y - fy0 + 0.5) * scaleY))
|
||||
if pixels[at] != colours[indices[y][x]]:
|
||||
wrong += 1
|
||||
cells = (fx1 + 1 - fx0) * (fy1 + 1 - fy0)
|
||||
print(f"{label}: icon shown at {sx0},{sy0} scaled {scaleX:.2f}x{scaleY:.2f}: {cells - wrong}/{cells} pixels right")
|
||||
if wrong:
|
||||
fails.append(f"{wrong} of the icon's {cells} pixels wrong on the screen")
|
||||
|
||||
palLog = os.path.join(work, "Prog", "joeylog.txt")
|
||||
if not os.path.exists(palLog) or "PALPROBE: done" not in open(palLog, encoding="ascii", errors="replace").read():
|
||||
fails.append("Palprobe's log is not in Prog/ with its last line: it did not run in its own folder")
|
||||
if os.path.exists(os.path.join(work, "joeylog.txt")):
|
||||
fails.append("a joeylog.txt is in the drive's root: Palprobe ran outside its folder")
|
||||
|
||||
if fails:
|
||||
print(f"{label}: FAIL - " + "; ".join(fails))
|
||||
sys.exit(1)
|
||||
print(f"{label}: PASS (icon.library reads a tool's icon, {wantStack} bytes of stack, {width}x{height}x{depth}, and draws every pixel of it; started from it as Workbench does, Palprobe ran in its own folder and its message came back)")
|
||||
PY
|
||||
"$(joeyTool amigaCheck)" icon "$work" "$want_stack" "$WBTOOL" "$label"
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
# keypad's 8, 2, 4, and 6 as the arrows, its Enter as Return, and the right
|
||||
# Alt as Alt, each held alone, and Up and the keypad's 8 held together with
|
||||
# either let go first, which must leave Up down (scripts/lib/keysGate.sh) --
|
||||
# read off KEYS's cells in FS-UAE's screenshots (scripts/lib/keysCells.py),
|
||||
# read off KEYS's cells in FS-UAE's screenshots (tests/gate/keysCells.c),
|
||||
# the stage found by its still empty verification row
|
||||
# (scripts/lib/keysRow.py).
|
||||
# (tests/gate/keysRow.c).
|
||||
#
|
||||
# Usage: scripts/verify-amiga-input.sh
|
||||
# Requires: fs-uae, Xvfb, xdotool; build/amiga/bin/Keys (make amiga);
|
||||
|
|
@ -100,7 +100,7 @@ if ! amigaShot ":$display_num" "$shotKey" "$work/shots" "$work/stage.png"; then
|
|||
echo "verify-amiga-input: FAIL - FS-UAE took no screenshot" >&2
|
||||
exit 1
|
||||
fi
|
||||
found=$(python3 "$repo/scripts/lib/keysRow.py" "$work/stage.png" "") || true
|
||||
found=$("$(joeyTool keysRow)" "$work/stage.png" "") || true
|
||||
if [[ ! "$found" =~ row\ at\ \(([0-9]+),([0-9]+)\)\ at\ ([0-9]+)x ]]; then
|
||||
echo "verify-amiga-input: FAIL - KEYS's stage is not on the screen: $found" >&2
|
||||
exit 1
|
||||
|
|
@ -118,7 +118,7 @@ keysRelease() {
|
|||
}
|
||||
keysRead() {
|
||||
if amigaShot ":$display_num" "$shotKey" "$work/shots" "$work/cells.png"; then
|
||||
python3 "$repo/scripts/lib/keysCells.py" "$work/cells.png" --at "$stageAt" --scale "$scale"
|
||||
"$(joeyTool keysCells)" "$work/cells.png" --at "$stageAt" --scale "$scale"
|
||||
else
|
||||
echo "no screenshot"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@
|
|||
# the boot drive carries Workbench's (component amigaLowlevel). FS-UAE's
|
||||
# ports are both in joystick mode, every line of each on a host key of its
|
||||
# own (keyboard_key_*), and xdotool holds the keys; FS-UAE's screenshots
|
||||
# (its screenshot action, on F11) go through scripts/lib/joyScreen.py
|
||||
# (its screenshot action, on F11) go through tests/gate/joyScreen.c
|
||||
# (scripts/lib/joyGate.sh). Escape must then end JOY.
|
||||
#
|
||||
# Usage: scripts/verify-amiga-joy.sh
|
||||
# Env: JOY_SHOTS also keep the screenshots in this folder
|
||||
# Requires: FS-UAE, the Kickstart, lowlevel.library (install emulator:amiga),
|
||||
# Xvfb, xdotool, python3 + PIL; build/amiga/bin/Joy (make amiga).
|
||||
# Xvfb, xdotool; build/amiga/bin/Joy (make amiga).
|
||||
set -uo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
|
|
@ -119,7 +119,7 @@ joyShot() {
|
|||
|
||||
# joyStage -- whether a screenshot now shows JOY's stage.
|
||||
joyStage() {
|
||||
joyShot "$JOY_WORK/probe.png" && python3 "$repo/scripts/lib/joyScreen.py" "$JOY_WORK/probe.png" > /dev/null 2>&1
|
||||
joyShot "$JOY_WORK/probe.png" && "$(joeyTool joyScreen)" "$JOY_WORK/probe.png" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -85,44 +85,4 @@ sleep 2
|
|||
DISPLAY=$display import -window root "$work/screen.png"
|
||||
grep "PALPROBE:" "$log"
|
||||
|
||||
python3 - "$work/screen.png" "$log" "$MIN_RATE" "$label" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
png, log, minRate, label = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]
|
||||
rates = {}
|
||||
for line in open(log, encoding="ascii", errors="replace"):
|
||||
m = re.search(r"PALPROBE: (palette|scb)\+present: \d+ in \d+ ms = (\d+)/s", line)
|
||||
if m:
|
||||
rates[m.group(1)] = int(m.group(2))
|
||||
if sorted(rates) != ["palette", "scb"]:
|
||||
print(f"{label}: FAIL - the log does not have both rates ({rates})")
|
||||
sys.exit(1)
|
||||
|
||||
# The bands down the middle column, top to bottom, black (the border)
|
||||
# dropped: each run of one colour is a band.
|
||||
img = Image.open(png).convert("RGB")
|
||||
w, h = img.size
|
||||
named = {(255, 0, 0): "red", (0, 255, 0): "green", (255, 255, 0): "yellow"}
|
||||
bands = []
|
||||
for y in range(h):
|
||||
rgb = img.getpixel((w // 2, y))
|
||||
if rgb == (0, 0, 0):
|
||||
continue
|
||||
name = named.get(rgb, "other " + str(rgb))
|
||||
if not bands or bands[-1] != name:
|
||||
bands.append(name)
|
||||
want = ["red", "green", "yellow", "red"]
|
||||
fails = []
|
||||
for kind in ("palette", "scb"):
|
||||
if rates[kind] < minRate:
|
||||
fails.append(f"{kind} changes presented {rates[kind]}/s, want at least {minRate}")
|
||||
if bands != want:
|
||||
fails.append(f"bands {bands}, want {want}")
|
||||
if fails:
|
||||
print(f"{label}: FAIL - " + "; ".join(fails))
|
||||
sys.exit(1)
|
||||
print(f"{label}: PASS (palette changes {rates['palette']}/s, SCB changes {rates['scb']}/s; bands {', '.join(want)} after a palette change and a same-layout SCB change)")
|
||||
PY
|
||||
"$(joeyTool amigaCheck)" palette "$work/screen.png" "$log" "$MIN_RATE" "$label"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
#
|
||||
# Boots SerTest (examples/sertest: banner, then echo) off a directory hard
|
||||
# drive in FS-UAE with the serial port on a TCP socket, and has the host
|
||||
# peer (scripts/lib/serialLoopPeer.py) wait for the banner, send a probe and
|
||||
# peer (tests/gate/serialLoopPeer.c) wait for the banner, send a probe and
|
||||
# expect it echoed. That exercises the chain: m68k-amigaos-gcc -> AmigaDOS ->
|
||||
# serial.device -> JoeyLib serial HAL -> Paula's UART -> host.
|
||||
#
|
||||
|
|
@ -60,7 +60,7 @@ HOME="$work/home" DISPLAY="$display" SDL_AUDIODRIVER=dummy ALSOFT_DRIVERS=null "
|
|||
> "$work/fsuae.log" 2>&1 &
|
||||
fsuaePid=$!
|
||||
|
||||
result=$(python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-client "$port" "$wall")
|
||||
result=$("$(joeyTool serialLoopPeer)" tcp-client "$port" "$wall")
|
||||
rc=$?
|
||||
echo "$result"
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|||
source "$repo/scripts/lib/joeyEnv.sh"
|
||||
joeyNeed HATARI hatari
|
||||
joeyNeed EMUTOS_IMG emutos
|
||||
stAudioGrade=$(joeyTool stAudioGrade) || exit 1
|
||||
machine=st
|
||||
if [ "${1:-}" = "--machine" ]; then
|
||||
machine=${2:-}
|
||||
|
|
@ -198,222 +199,4 @@ sleep 1
|
|||
|
||||
[ -s "$work/ym.log" ] || { echo "verify-atarist-audio: FAIL - empty PSG trace" >&2; exit 1; }
|
||||
|
||||
python3 - "$work/ym.log" "$work/baseline.log" "$prog" "$taps" "$machine" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
# ym write data reg=0x8 val=0xc video_cyc=8696 60@17 pc=e1197e instr_cycle 16
|
||||
LINE = re.compile(r'^ym write data reg=0x([0-9a-f]+) val=0x([0-9a-f]+) '
|
||||
r'video_cyc=(\d+).*pc=([0-9a-f]+)')
|
||||
|
||||
PUMP_PER_FRAME_MIN = 100 # tracker writes <= 1; the pump writes 205-246
|
||||
PUMP_RATE_LO = 150 # 12288/60 = 205, 12288/50 = 246. Below this the
|
||||
PUMP_RATE_HI = 280 # Timer A divider is wrong, not the sound.
|
||||
STRAY_FRAME_PCT_MAX = 5.0 # start/stop transitions only; a fighting
|
||||
# tracker touches reg 8 EVERY frame.
|
||||
REG_VOL_A = 8
|
||||
REG_MIXER = 7
|
||||
TONE_B_REGS = (2, 3) # channel B period == the tracker's own voice
|
||||
VOICE_A_REGS = (0, 1, 8) # channel A period + volume == the tracker's voice 0
|
||||
|
||||
# STe DMA sound: "DMA snd mode write: 0x01 mode=stereo freq=12517 ... pc=22eec"
|
||||
DMA_WRITE = re.compile(r'^DMA snd (?:mode|control) write: 0x([0-9a-f]+) .*pc=([0-9a-f]+)')
|
||||
DMA_MODE_STEREO_12K = 0x01 # bit 7 clear stereo, rate 1 = 12517 Hz
|
||||
DMA_CTRL_PLAY_LOOP = 0x03 # play + repeat
|
||||
DMA_UNCONVERTED_SILENCE = -128 # $80, the mixer's unsigned silence, read as signed
|
||||
SFX_SAMPLES_MIN = 100 # test.sfx is ~390 non-silent samples at 12517 Hz
|
||||
SFX_PEAK_MIN = 32 # it peaks near half scale, the mix's unit (61 measured)
|
||||
UNSIGNED_PCT_MAX = 5.0
|
||||
|
||||
|
||||
def perFrameWrites(path):
|
||||
"""{frame: {reg: [pc, ...]}} for APPLICATION writes only."""
|
||||
out = {}
|
||||
frame = 0
|
||||
prev = -1
|
||||
for line in open(path, errors='ignore'):
|
||||
m = LINE.match(line)
|
||||
if m is None:
|
||||
continue
|
||||
reg = int(m.group(1), 16)
|
||||
cyc = int(m.group(3))
|
||||
pc = m.group(4)
|
||||
if cyc < prev: # video_cyc restarts every frame
|
||||
frame += 1
|
||||
prev = cyc
|
||||
if pc.startswith('e'): # TOS, not us
|
||||
continue
|
||||
out.setdefault(frame, {}).setdefault(reg, []).append(pc)
|
||||
return out
|
||||
|
||||
|
||||
def countReg(frames, table, regs):
|
||||
return sum(len(table[f].get(r, ())) for f in frames for r in regs)
|
||||
|
||||
|
||||
full = perFrameWrites(sys.argv[1])
|
||||
base = perFrameWrites(sys.argv[2])
|
||||
prog = sys.argv[3]
|
||||
taps = int(sys.argv[4])
|
||||
machine = sys.argv[5]
|
||||
if not full:
|
||||
print("verify-atarist-audio: FAIL - no application PSG writes at all", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
frames = sorted(full)
|
||||
fails = []
|
||||
|
||||
# 1. The tracker ran before any SFX overlapped it.
|
||||
baseFrames = sorted(base)
|
||||
baseTone = countReg(baseFrames, base, TONE_B_REGS)
|
||||
baseMixer = countReg(baseFrames, base, (REG_MIXER,))
|
||||
if baseTone == 0 or baseMixer == 0:
|
||||
fails.append(f"the tracker never played: {baseTone} channel-B period writes and "
|
||||
f"{baseMixer} mixer writes before the SFX. jlMusicPlay did not drive the PSG")
|
||||
|
||||
|
||||
def gradePump(summary):
|
||||
"""Plain ST: the PWM pump through channel A's volume, checks 2-4."""
|
||||
# 2. The pump ran, at the right rate. Group pump frames into contiguous runs
|
||||
# so each SFX can be told from the next.
|
||||
runs = []
|
||||
current = []
|
||||
for f in frames:
|
||||
if len(full[f].get(REG_VOL_A, ())) >= PUMP_PER_FRAME_MIN:
|
||||
current.append(f)
|
||||
elif current:
|
||||
runs.append(current)
|
||||
current = []
|
||||
if current:
|
||||
runs.append(current)
|
||||
|
||||
peak = max((len(full[f].get(REG_VOL_A, ())) for f in frames), default=0)
|
||||
if not runs:
|
||||
fails.append(f"the PWM pump never ran: peak {peak} reg-8 writes in a frame, want "
|
||||
f">= {PUMP_PER_FRAME_MIN}. jlAudioPlaySfx did not start Timer A, so "
|
||||
f"every digital sound on this port is silent")
|
||||
elif not (PUMP_RATE_LO <= peak <= PUMP_RATE_HI):
|
||||
fails.append(f"the pump ran at {peak} reg-8 writes/frame, want {PUMP_RATE_LO}-{PUMP_RATE_HI} "
|
||||
f"(12288 Hz / 50-60 fps). The MFP Timer A divider is wrong, so sampled "
|
||||
f"sound plays at the wrong pitch")
|
||||
|
||||
if runs:
|
||||
# 3 + 4. Inside each run, minus its first and last frame.
|
||||
interior = [f for r in runs for f in (r[1:-1] if len(r) > 2 else [])]
|
||||
writers = {}
|
||||
for f in interior:
|
||||
for pc in full[f].get(REG_VOL_A, ()):
|
||||
writers[pc] = writers.get(pc, 0) + 1
|
||||
ranked = sorted(writers.items(), key=lambda kv: -kv[1])
|
||||
isr = ranked[0][0] if ranked else None
|
||||
strayFrames = sum(1 for f in interior
|
||||
if any(pc != isr for pc in full[f].get(REG_VOL_A, ())))
|
||||
strayPct = (100.0 * strayFrames / len(interior)) if interior else 0.0
|
||||
if strayPct > STRAY_FRAME_PCT_MAX:
|
||||
others = ", ".join(f"pc={pc} x{n}" for pc, n in ranked[1:4])
|
||||
fails.append(f"reg 8 (volume A) has {len(ranked)} writers in {strayPct:.1f}% of the "
|
||||
f"pump's frames: ISR pc={isr} plus {others}. The tracker is overwriting "
|
||||
f"the PWM sample - digital and chiptune are colliding on channel A")
|
||||
|
||||
toneDuring = countReg(interior, full, TONE_B_REGS)
|
||||
if toneDuring == 0:
|
||||
fails.append("the tracker stopped writing its own channel B while the pump ran, so "
|
||||
"the SFX silenced the music instead of mixing with it")
|
||||
|
||||
summary += (f"; pump {len(runs)} runs (expected {taps}) peak {peak} reg8/frame, "
|
||||
f"{len(writers)} reg8 writer(s), stray frames {strayPct:.1f}%, "
|
||||
f"tracker toneB during pump {toneDuring}")
|
||||
else:
|
||||
summary += f"; pump never ran (peak {peak} reg8/frame)"
|
||||
return summary
|
||||
|
||||
|
||||
def gradeDma(summary):
|
||||
"""STe: the DMA chip plays the digital sound and the PSG keeps channel A."""
|
||||
modes = []
|
||||
controls = []
|
||||
runs = []
|
||||
run = None
|
||||
for line in open(sys.argv[1], errors='ignore'):
|
||||
if line.startswith('DMA snd mode write:'):
|
||||
m = DMA_WRITE.match(line)
|
||||
if m and not m.group(2).startswith('e'):
|
||||
modes.append(int(m.group(1), 16))
|
||||
elif line.startswith('DMA snd control write: starting'):
|
||||
run = {'samples': [], 'ym': {}}
|
||||
runs.append(run)
|
||||
elif line.startswith('DMA snd control write: stopping'):
|
||||
run = None
|
||||
elif line.startswith('DMA snd control write:'):
|
||||
m = DMA_WRITE.match(line)
|
||||
if m and not m.group(2).startswith('e'):
|
||||
controls.append(int(m.group(1), 16))
|
||||
elif run is None:
|
||||
continue
|
||||
elif line.startswith('DMA snd fifo pull'):
|
||||
v = int(line.split()[-1], 16)
|
||||
run['samples'].append(v - 256 if v > 127 else v)
|
||||
else:
|
||||
m = LINE.match(line)
|
||||
if m and not m.group(4).startswith('e'):
|
||||
reg = int(m.group(1), 16)
|
||||
run['ym'][reg] = run['ym'].get(reg, 0) + 1
|
||||
|
||||
# 2. The chip ran, programmed as the HAL programs it.
|
||||
if not runs:
|
||||
fails.append("the DMA sound chip never started: jlAudioPlaySfx did not reach it, so "
|
||||
"every digital sound on the STe is silent")
|
||||
if not modes or any(m != DMA_MODE_STEREO_12K for m in modes):
|
||||
fails.append(f"DMA mode writes {[hex(m) for m in modes]}, want {hex(DMA_MODE_STEREO_12K)} "
|
||||
f"(stereo, 12517 Hz) -- the mix is made for that rate and layout")
|
||||
if DMA_CTRL_PLAY_LOOP not in controls:
|
||||
fails.append(f"DMA control writes {[hex(c) for c in controls]} never set play + repeat "
|
||||
f"({hex(DMA_CTRL_PLAY_LOOP)}), so the chip cannot loop over the mix buffer")
|
||||
|
||||
# 3. Each run played the SFX.
|
||||
played = [sum(1 for v in r['samples'] if v != 0) for r in runs]
|
||||
peaks = [max((abs(v) for v in r['samples']), default=0) for r in runs]
|
||||
pulled = sum(len(r['samples']) for r in runs)
|
||||
unsignedSilence = sum(1 for r in runs for v in r['samples'] if v == DMA_UNCONVERTED_SILENCE)
|
||||
if runs and (min(played) < SFX_SAMPLES_MIN or min(peaks) < SFX_PEAK_MIN):
|
||||
fails.append(f"a DMA run played no SFX: non-silent samples per run {played} (want >= "
|
||||
f"{SFX_SAMPLES_MIN}), peaks {peaks} (want >= {SFX_PEAK_MIN}). The mix never "
|
||||
f"reached the buffer the chip plays")
|
||||
if pulled and 100.0 * unsignedSilence / pulled > UNSIGNED_PCT_MAX:
|
||||
fails.append(f"{unsignedSilence} of {pulled} samples are -128: unsigned silence the HAL "
|
||||
f"never converted to the chip's signed format, a DC step rather than a wave")
|
||||
|
||||
# 4. No PWM pump on this machine.
|
||||
peak = max((len(full[f].get(REG_VOL_A, ())) for f in frames), default=0)
|
||||
if peak >= PUMP_PER_FRAME_MIN:
|
||||
fails.append(f"reg 8 was written {peak} times in one frame: the PWM pump ran on a "
|
||||
f"machine with DMA sound, so channel A is still the DAC")
|
||||
|
||||
# 5. The tracker keeps channel A (and B) while the chip plays.
|
||||
voiceA = sum(r['ym'].get(reg, 0) for r in runs for reg in VOICE_A_REGS)
|
||||
toneB = sum(r['ym'].get(reg, 0) for r in runs for reg in TONE_B_REGS)
|
||||
if runs and voiceA == 0:
|
||||
fails.append("the tracker wrote none of voice 0's registers (0, 1, 8) while the DMA chip "
|
||||
"played: channel A is still being given up to digital sound")
|
||||
if runs and toneB == 0:
|
||||
fails.append("the tracker stopped writing its own channel B while the DMA chip played, "
|
||||
"so the SFX silenced the music instead of mixing with it")
|
||||
|
||||
return (summary + f"; DMA {len(runs)} runs (expected {taps}), mode "
|
||||
f"{','.join(sorted(set(hex(m) for m in modes)))}, non-silent samples/run "
|
||||
f"{min(played, default=0)}..{max(played, default=0)}, peak "
|
||||
f"{max(peaks, default=0)}, peak reg8/frame {peak}, tracker voice A {voiceA} / "
|
||||
f"toneB {toneB} writes during DMA")
|
||||
|
||||
|
||||
summary = f"{prog} tracker baseline {baseTone} toneB/{baseMixer} mixer"
|
||||
summary = gradeDma(summary) if machine == 'ste' else gradePump(summary)
|
||||
|
||||
print(f"verify-atarist-audio: {summary}")
|
||||
for f in fails:
|
||||
print(f"verify-atarist-audio: {f}", file=sys.stderr)
|
||||
if fails:
|
||||
print("verify-atarist-audio: FAIL", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("verify-atarist-audio: PASS")
|
||||
PY
|
||||
"$stAudioGrade" "$work/ym.log" "$work/baseline.log" "$prog" "$taps" "$machine"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ source "$repo/scripts/lib/keysGate.sh"
|
|||
joeyNeed HATARI hatari
|
||||
joeyNeed EMUTOS_IMG emutos
|
||||
joeyNeed EMUTOS_DE_IMG emutos
|
||||
stScreenDump=$(joeyTool stScreenDump) || exit 1
|
||||
bin_dir=$repo/build/atarist/bin
|
||||
label=verify-atarist-input
|
||||
|
||||
|
|
@ -122,43 +123,7 @@ rowCheck() {
|
|||
sleep 3
|
||||
send "hatari-debug m \$$(printf '%x' "$rowAddr") 160"
|
||||
sleep 2
|
||||
python3 - "$work/hatari.log" "$rowAddr" "$1" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
log, rowAddr, target = sys.argv[1], int(sys.argv[2]), sys.argv[3]
|
||||
data = open(log, "rb").read().decode(errors="replace")
|
||||
lines = {}
|
||||
# Keep only the LAST dump of each address (earlier sentinel probes also match).
|
||||
for m in re.finditer(r"^(00[0-9A-F]{6}): ((?:[0-9a-f]{2} ){1,16})", data, re.M):
|
||||
lines[int(m.group(1), 16)] = m.group(2)
|
||||
raw = b""
|
||||
for off in range(0, 160, 16):
|
||||
if rowAddr + off not in lines:
|
||||
print(f"verify-atarist-input: FAIL - missing dump line at +{off}")
|
||||
sys.exit(1)
|
||||
raw += bytes.fromhex(lines[rowAddr + off].replace(" ", ""))
|
||||
|
||||
def group(words):
|
||||
p = [int.from_bytes(words[i * 2:i * 2 + 2], "big") for i in range(4)]
|
||||
return [((p[0] >> b) & 1) | (((p[1] >> b) & 1) << 1) |
|
||||
(((p[2] >> b) & 1) << 2) | (((p[3] >> b) & 1) << 3)
|
||||
for b in range(15, -1, -1)]
|
||||
|
||||
pix = []
|
||||
for g in range(len(raw) // 8):
|
||||
pix += group(raw[g * 8:g * 8 + 8])
|
||||
|
||||
if pix[0:4] != [0xA, 0x5, 0xA, 0x5]:
|
||||
print(f"verify-atarist-input: FAIL - sentinel wrong: {pix[0:4]}")
|
||||
sys.exit(1)
|
||||
count = (pix[4] << 4) | pix[5]
|
||||
chars = bytes((pix[8 + 2 * i] << 4) | pix[8 + 2 * i + 1] for i in range(count))
|
||||
print(f"VERIFY-ST-INPUT count={count} chars={chars!r}")
|
||||
if chars.decode("ascii", errors="replace") != target:
|
||||
print(f'verify-atarist-input: FAIL - expected "{target}"')
|
||||
sys.exit(1)
|
||||
PY
|
||||
"$stScreenDump" keys "$work/hatari.log" "$rowAddr" "$1"
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -195,28 +160,7 @@ keysRelease() {
|
|||
keysRead() {
|
||||
send "hatari-debug m \$$(printf '%x' "$cellAddr") 160"
|
||||
sleep 1
|
||||
python3 - "$work/hatari.log" "$cellAddr" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
|
||||
log, addr = sys.argv[1], int(sys.argv[2])
|
||||
GROUPS = (5, 7, 9, 11, 13, 15, 17)
|
||||
data = open(log, "rb").read().decode(errors="replace")
|
||||
lines = {}
|
||||
for m in re.finditer(r"^(00[0-9A-F]{6}): ((?:[0-9a-f]{2} ){1,16})", data, re.M):
|
||||
lines[int(m.group(1), 16)] = m.group(2)
|
||||
raw = b""
|
||||
for off in range(0, 160, 16):
|
||||
raw += bytes.fromhex(lines.get(addr + off, "00 " * 16).replace(" ", ""))
|
||||
digits = ""
|
||||
for g in GROUPS:
|
||||
colour = 0
|
||||
for plane in range(4):
|
||||
word = int.from_bytes(raw[g * 8 + plane * 2:g * 8 + plane * 2 + 2], "big")
|
||||
colour |= ((word >> 15) & 1) << plane
|
||||
digits += "1" if colour == 2 else ("0" if colour == 1 else "?")
|
||||
print(digits)
|
||||
PY
|
||||
"$stScreenDump" cells "$work/hatari.log" "$cellAddr"
|
||||
}
|
||||
KEYS_SETTLE_SECONDS=1
|
||||
keysRunSteps kp8 kp2 kp4 kp6 kpenter
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@
|
|||
# Hatari's [Joystick0] and [Joystick1] (a config file of the gate's own) are
|
||||
# keyboard joysticks on host keys, which xdotool holds on a private Xvfb
|
||||
# display; Hatari's own screenshots (its command FIFO's screenshot shortcut)
|
||||
# go through scripts/lib/joyScreen.py (scripts/lib/joyGate.sh). Escape must
|
||||
# go through tests/gate/joyScreen.c (scripts/lib/joyGate.sh). Escape must
|
||||
# then end JOY.
|
||||
#
|
||||
# Usage: scripts/verify-atarist-joy.sh [atarist|ste] [MACHINE]
|
||||
# (default atarist on an st; ste on an ste)
|
||||
# Env: JOY_SHOTS also keep the screenshots in this folder
|
||||
# Requires: Hatari, EmuTOS, Xvfb, xdotool, python3 + PIL;
|
||||
# Requires: Hatari, EmuTOS, Xvfb, xdotool;
|
||||
# build/<build>/bin/JOY.PRG (make atarist / make ste).
|
||||
set -uo pipefail
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ joeyNeed HATARI hatari
|
|||
joeyNeed EMUTOS_IMG emutos
|
||||
|
||||
build="${1:-atarist}"
|
||||
# The ST build writes 3-bit colours on any machine (joyScreen.py --st).
|
||||
# The ST build writes 3-bit colours on any machine (joyScreen --st).
|
||||
case "$build" in
|
||||
atarist) machine="${2:-st}"; JOY_SCREEN_ARGS=(--st) ;;
|
||||
ste) machine="${2:-ste}" ;;
|
||||
|
|
@ -184,7 +184,7 @@ joyShot() {
|
|||
|
||||
# joyStage -- whether a screenshot now shows JOY's stage.
|
||||
joyStage() {
|
||||
joyShot "$JOY_WORK/probe.png" && python3 "$repo/scripts/lib/joyScreen.py" "$JOY_WORK/probe.png" "${JOY_SCREEN_ARGS[@]}" > /dev/null 2>&1
|
||||
joyShot "$JOY_WORK/probe.png" && "$(joeyTool joyScreen)" "$JOY_WORK/probe.png" "${JOY_SCREEN_ARGS[@]}" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -71,58 +71,79 @@ for machine in $machines; do
|
|||
--auto 'C:\PATTERN.PRG' 2>&1 \
|
||||
| grep -E '^write col addr=ff82[45]' | grep -v ' pc=e' > "$work/$machine.log"
|
||||
|
||||
python3 - "$work/$machine.log" "$machine" "$build" <<'PY' || fails=$((fails + 1))
|
||||
import re
|
||||
import sys
|
||||
|
||||
# write col addr=ff825e col=70 video_cyc_w=55892 ... pc=1f00e instr_cyc=12
|
||||
LINE = re.compile(r'^write col addr=ff82([45][0-9a-f]) col=([0-9a-f]+) ')
|
||||
PALETTE_BASE = 0x40
|
||||
COLORS = 16
|
||||
# PATTERN's eight palettes: which of (red, green, blue) carry the gradient.
|
||||
GRADIENTS = [(1, 1, 1), (1, 0, 0), (1, 1, 0), (0, 1, 0),
|
||||
(0, 1, 1), (0, 0, 1), (1, 0, 1), (1, 1, 1)]
|
||||
|
||||
|
||||
def channel(build, v):
|
||||
"""One 4-bit channel as that build writes the shifter nibble."""
|
||||
if build == 'atarist':
|
||||
return v >> 1
|
||||
return (v >> 1) | ((v & 1) << 3)
|
||||
|
||||
|
||||
path, machine, build = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
seen = [set() for _ in range(COLORS)]
|
||||
writes = 0
|
||||
for line in open(path, errors='ignore'):
|
||||
m = LINE.match(line)
|
||||
if m:
|
||||
seen[(int(m.group(1), 16) - PALETTE_BASE) // 2].add(int(m.group(2), 16))
|
||||
writes += 1
|
||||
|
||||
bad = []
|
||||
for i in range(COLORS):
|
||||
want = set()
|
||||
for r, g, b in GRADIENTS:
|
||||
v = channel(build, i) if i else 0
|
||||
want.add(((v if r else 0) << 8) | ((v if g else 0) << 4) | (v if b else 0))
|
||||
if seen[i] != want:
|
||||
bad.append(f"register {i}: wrote {sorted(hex(v) for v in seen[i])}, "
|
||||
f"want {sorted(hex(v) for v in want)}")
|
||||
|
||||
bits = '3-bit' if build == 'atarist' else '4-bit'
|
||||
if writes == 0:
|
||||
print(f"verify-atarist-palette: {build} build on {machine} FAIL - PATTERN never wrote the palette", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if bad:
|
||||
print(f"verify-atarist-palette: {build} build on {machine} FAIL - not the {bits} encoding of PATTERN's palettes:",
|
||||
file=sys.stderr)
|
||||
for b in bad[:4]:
|
||||
print(f"verify-atarist-palette: {b}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"verify-atarist-palette: {build} build on {machine} OK - {writes} palette writes, all 16 "
|
||||
f"registers exactly the {bits} encoding of the 8 band palettes")
|
||||
PY
|
||||
awk -v machine="$machine" -v build="$build" '
|
||||
# One 4-bit channel as that build writes the shifter nibble.
|
||||
function channel(v) {
|
||||
return build == "atarist" ? rshift(v, 1) : or(rshift(v, 1), lshift(and(v, 1), 3))
|
||||
}
|
||||
# A set of colour words as Python prints a sorted list of their hex.
|
||||
function hexList(set, count, names, i, n, out) {
|
||||
n = 0
|
||||
for (i = 0; i < count; i++) {
|
||||
names[++n] = sprintf("0x%x", set[i])
|
||||
}
|
||||
asort(names)
|
||||
out = ""
|
||||
for (i = 1; i <= n; i++) {
|
||||
out = out (i > 1 ? ", " : "") "\x27" names[i] "\x27"
|
||||
}
|
||||
return "[" out "]"
|
||||
}
|
||||
BEGIN {
|
||||
# PATTERN'"'"'s eight palettes: which of red, green, and blue carry
|
||||
# the gradient.
|
||||
split("111 100 110 010 011 001 101 111", gradients, " ")
|
||||
}
|
||||
# write col addr=ff825e col=70 video_cyc_w=55892 ... pc=1f00e
|
||||
match($0, /^write col addr=ff82([45][0-9a-f]) col=([0-9a-f]+) /, m) {
|
||||
register = int((strtonum("0x" m[1]) - 64) / 2)
|
||||
value = strtonum("0x" m[2])
|
||||
if (!((register, value) in seen)) {
|
||||
seen[register, value] = 1
|
||||
got[register, seenCount[register]++] = value
|
||||
}
|
||||
writes++
|
||||
}
|
||||
END {
|
||||
bits = build == "atarist" ? "3-bit" : "4-bit"
|
||||
if (writes == 0) {
|
||||
printf "verify-atarist-palette: %s build on %s FAIL - PATTERN never wrote the palette\n", build, machine > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
badCount = 0
|
||||
for (i = 0; i < 16; i++) {
|
||||
delete want
|
||||
delete wantList
|
||||
wantCount = 0
|
||||
v = i ? channel(i) : 0
|
||||
for (g = 1; g <= 8; g++) {
|
||||
word = (substr(gradients[g], 1, 1) == "1" ? lshift(v, 8) : 0) + (substr(gradients[g], 2, 1) == "1" ? lshift(v, 4) : 0) + (substr(gradients[g], 3, 1) == "1" ? v : 0)
|
||||
if (!(word in want)) {
|
||||
want[word] = 1
|
||||
wantList[wantCount++] = word
|
||||
}
|
||||
}
|
||||
same = wantCount == seenCount[i]
|
||||
for (word in want) {
|
||||
same = same && ((i, word + 0) in seen)
|
||||
}
|
||||
if (!same) {
|
||||
delete gotList
|
||||
for (k = 0; k < seenCount[i]; k++) {
|
||||
gotList[k] = got[i, k]
|
||||
}
|
||||
bad[++badCount] = sprintf("register %d: wrote %s, want %s", i, hexList(gotList, seenCount[i]), hexList(wantList, wantCount))
|
||||
}
|
||||
}
|
||||
if (badCount > 0) {
|
||||
printf "verify-atarist-palette: %s build on %s FAIL - not the %s encoding of PATTERN'"'"'s palettes:\n", build, machine, bits > "/dev/stderr"
|
||||
for (b = 1; b <= badCount && b <= 4; b++) {
|
||||
printf "verify-atarist-palette: %s\n", bad[b] > "/dev/stderr"
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
printf "verify-atarist-palette: %s build on %s OK - %d palette writes, all 16 registers exactly the %s encoding of the 8 band palettes\n", build, machine, writes, bits
|
||||
}' "$work/$machine.log" || fails=$((fails + 1))
|
||||
done
|
||||
|
||||
if [ "$fails" -gt 0 ]; then
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue