diff --git a/README.md b/README.md index 2356147d..715bcc86 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Recommended reading to get you started: | GARLIC | NO | NO |NO | No longer maintained, use Portmaster| | GARLICPLUS | MAYBE | NO | YES | Port by [Simotek](http://simotek.net)| | RG35XXPLUS | MAYBE | NO | YES | Port by [Simotek](http://simotek.net)| +| RK3566 | MAYBE | YES | YES | ROCKNIX handhelds (Powkiddy RGB30 etc.), via the ALSA sequencer [See notes](docs/RK3566_PORT.md) | | MACOS | YES | YES | MAYBE | Port by [clsource](https://genserver.social/clsource) | * **MIDI functionality __greatly__ depends on kernel support, please feature request your favourite OS maintainer =)** diff --git a/docs/RK3566_PORT.md b/docs/RK3566_PORT.md new file mode 100644 index 00000000..795f0327 --- /dev/null +++ b/docs/RK3566_PORT.md @@ -0,0 +1,474 @@ +# LittleGPTracker on RK3566 / ROCKNIX + +Port of LittleGPTracker to RK3566 based handhelds running +[ROCKNIX](https://rocknix.org) — the Powkiddy RGB30, Anbernic RG-ARC, Gameforce +Chi and friends. It builds on an x86_64 host and deploys as a single executable: +no bundled libraries, no bundled fonts, no bundled C library. + +## What the port consists of + +| File | Purpose | +|------|---------| +| `projects/Makefile.RK3566` | Build configuration: toolchain, sysroot, defines | +| `projects/Makefile` | Two added lines registering `RK3566DIRS` / `RK3566FILES` | +| `projects/resources/RK3566/config.xml` | Fullscreen, panel fill, quit hotkey, auto-load, MIDI input, ayu dark theme | +| `projects/resources/RK3566/mapping.xml` | Gamepad button mapping | +| `projects/resources/RK3566/LittleGPTracker.sh` | Launch script for the EmulationStation "Ports" menu | +| `projects/resources/RK3566/setup-sysroot.sh` | Builds the cross sysroot (SDL2 + ALSA) from a device over ssh | +| `projects/resources/RK3566/fetch-demo-song.sh` | Installs a demo song from the official release | + +Five changes were needed in shared code, each marked with a comment: + +* `sources/Application/Application.cpp` — `MIDICTRLDEVICE` accepts `*` to mean + "every MIDI input that is present". A device name has to be known in advance + to be written into a config file, and on a handheld it is not: it is whatever + keyboard the user plugs in. See "MIDI" below. + +* `sources/Adapters/SDL2/Audio/SDLAudioDriver.cpp` — added the missing + ``: the file uses `memcpy`/`memmove` and relied on a transitive + include that newer libstdc++ headers no longer provide. +* `sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp` — drawing and presentation are + now separate. The tracker draws its 320x240 screen at 1:1 into a fixed surface + and `Flush()` scales that onto the window with `SDL_BlitScaled`. Previously the + screen was drawn straight into the window at a whole-number multiple, which on + a 720x720 panel capped the scale at 2x and produced unreadably small text (an + 8x8 glyph is about 1.6 mm on that display). See "Display" below. +* `sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp` — opt-in `LGPT_SCREENSHOT` + diagnostic (see below). +* `sources/Adapters/SDL2/GUI/SDLEventManager.{h,cpp}` — a configurable quit + hotkey (`QUITBUTTONS`), and a fix for joystick event indexing. SDL2 reports a + joystick *instance id* in `event.jbutton.which`, but the code used it directly + as an index into four-element arrays; with a hotplugged second controller the + instance id runs past the end of those arrays. Events are now mapped back to + the index they were opened with. + +## Target + +| | | +|---|---| +| Device verified on | Powkiddy RGB30 | +| OS | ROCKNIX 20240815, kernel 6.9.12 | +| SoC | Rockchip RK3566, 4x Cortex-A55 (aarch64) | +| Display | 720x720 DSI panel, sway/Wayland compositor | +| Audio | PipeWire + PipeWire-Pulse, ALSA underneath | +| Gamepad | `retrogame_joypad` (rocknix-singleadc-joypad) on `/dev/input/js0` | +| glibc / libstdc++ | 2.38 / GCC 13 (`GLIBCXX_3.4.33`) | +| SDL2 | 2.30.5 | +| MIDI | ALSA sequencer (`/dev/snd/seq`), libasound 1.2.10 | + +ROCKNIX mounts `/` read-only from a squashfs image; `/storage` is the writable +ext4 partition. Ports therefore live under `/storage/roms/ports/`. + +## How it builds + +The tracker is cross-compiled but linked against the **target's own SDL2 and +ALSA**. That keeps the deliverable to one file and guarantees the app runs +against exactly the SDL2 the compositor and audio server were tested with. Two +things are needed on the build host: + +1. **Arm GNU Toolchain** `aarch64-none-linux-gnu`, glibc **2.38**. The device + exports `GLIBC_2.38` as its newest versioned symbol; a newer toolchain emits + symbols the device's loader rejects. +2. **A sysroot** holding SDL2 2.30.x headers and the device's `libSDL2.so` for + the linker, plus alsa-lib headers and the device's `libasound.so` for MIDI. + A runtime image ships no headers, so the script fetches the alsa-lib release + matching the device's library and copies the library itself off the device. + `projects/resources/RK3566/setup-sysroot.sh` assembles all of this over ssh: + +```sh +cd projects/resources/RK3566 +./setup-sysroot.sh /opt/rocknix-sysroot root@RK3566.lan +``` + +Then: + +```sh +cd projects +make PLATFORM=RK3566 \ + RK3566_TOOLCHAIN=/opt/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu \ + RK3566_SYSROOT=/opt/rocknix-sysroot +``` + +The result is `projects/lgpt-rk3566.elf`. Build defines are `_64BIT`, +`CPP_MEMORY`, `SDL2`, `SDLAUDIO`, `RTMIDI`, `__LINUX_ALSA__`, +`__LINUX_ALSASEQ__` and `_NO_JACK_`, compiled `-std=gnu++03 -O3 +-mcpu=cortex-a55`. `FFMPEG_ENABLED` is deliberately off: it only gates the +ffmpeg based sample importer, which needs a keyboard and file dialog to be worth +anything. + +## How it deploys + +```sh +DEST=/storage/roms/ports/LittleGPTracker +scp projects/lgpt-rk3566.elf \ + projects/resources/RK3566/config.xml \ + projects/resources/RK3566/mapping.xml \ + projects/resources/RK3566/LittleGPTracker.sh \ + root@RK3566.lan:$DEST/ +ssh root@RK3566.lan "chmod +x $DEST/LittleGPTracker.sh $DEST/lgpt-rk3566.elf" +``` + +`config.xml` is read from the directory holding the executable, so it has to sit +next to the binary — not merely in the working directory. Projects and samples +are created in the working directory, which the launch script sets to the same +place. + +## Input mapping + +The RGB30's D-pad arrives as four ordinary buttons, not as a hat, so it is mapped +with `but:` rather than `hat:`. Button numbers are raw SDL joystick indices; +SDL numbers this pad in ascending evdev key code order, which matches +EmulationStation's own `es_input.cfg` for `retrogame_joypad`: + +| Index | evdev | Physical | +|-------|-------|----------| +| 0 | `BTN_SOUTH` | B (bottom face button) | +| 1 | `BTN_EAST` | A (right face button) | +| 2 | `BTN_NORTH` | Y | +| 3 | `BTN_WEST` | X | +| 4 / 5 | `BTN_TL` / `BTN_TR` | L1 / R1 | +| 6 / 7 | `BTN_TL2` / `BTN_TR2` | L2 / R2 | +| 8 / 9 | `BTN_SELECT` / `BTN_START` | Select / Start | +| 10 | `BTN_MODE` | Guide | +| 11 / 12 | `BTN_THUMBL` / `BTN_THUMBR` | L3 / R3 | +| 13–16 | `BTN_DPAD_UP/DOWN/LEFT/RIGHT` | D-pad | + +The left analog stick is mapped to the same four directions as a convenience. +Delete those four `joy:` lines from `mapping.xml` if stick drift ever produces +phantom presses. + +To re-derive the numbers on different hardware, set `DUMPEVENT` to `YES` in +`config.xml`, press a button and read the `but(N):M` lines in `log.txt`, where +`M` is the button index. + +### Quitting + +**Hold Select + Start.** This is the only control that works from anywhere, +because the tracker's own quit control — the `Exit` button — lives on the +project picker and cannot be reached once a project is loaded, and a handheld +has no window to close. `config.xml` therefore sets: + +```xml + +``` + +which is the SDL button index list that quits when held together, and + +```xml + +``` + +so the project picker (and with it a second way out) is always shown at +startup. Both are worth keeping unless you deliberately want to boot straight +into a project. + +## MIDI + +MIDI runs over the ALSA sequencer. `RtMidi` (the copy in `sources/Externals`) +is built with its `__LINUX_ALSASEQ__` backend, which means it talks to +`/dev/snd/seq` through libasound rather than to any audio server. Everything +that registers a port there is therefore usable: a USB MIDI interface +(`snd-usb-audio` is built into this kernel), anything PipeWire bridges into the +sequencer, or `aseqnet` for MIDI over the network. + +**Input.** `config.xml` sets: + +```xml + +``` + +`MIDICTRLDEVICE` is matched as a prefix against the port name the driver +publishes, and `*` is the wildcard that opens every input present. That matters +on a handheld: the name of the interface is not known when the config file is +written, and without a match the tracker opens no MIDI input at all. Interfaces +nothing is mapped to cost nothing. + +What an input device then does is decided by `mapping.xml`, where MIDI appears +as the controller source `midi`: + +```xml + +``` + +is MIDI channel 1, note 60. The middle field is the device name from `log.txt` +or `all` for any device; `note`, `cc`, `pb`, `pc` and `at` are the event types. +`docs/LittlePiggyTrackerConf.md` has the original write-up of this scheme. + +**Output.** Which interface the tracker sends to is a *project* setting, the +`MIDI:` line in the project view, and notes come from MIDI instruments. The list +of interfaces is read when the project is opened, so an interface that is +plugged in later is not offered until the project is reloaded. + +Ports are enumerated once, at startup, and the names found are written to +`log.txt`: + +``` +[RTMidiService] 1 input port(s) +[RTMidiService] Midi Through:0 +[RTMidiService] 1 output port(s) +[RTMidiService] Midi Through:0 +``` + +so plug the interface in before starting the tracker, and read that line to find +out what to put in `mapping.xml`. + +### The USB port + +The SoC has a host controller, but on the RGB30 ROCKNIX configures the port the +USB-C connector is wired to as a **device**: the device tree reports + +```sh +cat /proc/device-tree/usb@fcc00000/dr_mode # peripheral +``` + +and ROCKNIX's own `/usr/bin/usbgadget` binds an MTP/CDC-ethernet gadget to that +same controller. A USB MIDI keyboard plugged in there would be talking to a +port that is itself acting as a USB device, so it will not be enumerated. A +second controller (`usb@fd000000`) is in `dr_mode=host` with its xHCI +registered, but whether a physical connector is wired to it is a question about +the shell, not the software, and it has not been tested here. + +Everything above the kernel is in place either way: the tracker sees a MIDI +interface as an ALSA sequencer port, and it does not matter whether the kernel +got that port from USB, Bluetooth or the network. + +### Testing MIDI without a MIDI keyboard + +`CONFIG_SND_SEQ_DUMMY` is built into this kernel, so there is always a +`Midi Through` client that echoes whatever is sent to it back out. That is +enough to exercise both directions over ssh: + +```sh +# listen to the loopback +aseqdump -p 14:0 & + +# send one note to the loopback, which the tracker is listening on +aplaymidi -p 14:0 note60.mid +``` + +with, in `mapping.xml`, + +```xml + +``` + +Put the `Midi Through:0` name in the project's `MIDI:` setting to test the +other direction; `aseqdump` then shows the start byte and the clock the tracker +sends while it plays. + +## Demo songs + +All five demo songs from the official 1.6.0-bacon20 release are installed in the +port directory, so there is something to listen to out of the box. Pick one from +the project list after starting the tracker — the picker strips the `lgpt_` +prefix — and press Start. + +| Demo | Song rows | Chains | Channels | Samples | Size | +|------|-----------|--------|----------|---------|------| +| `lgpt_Bootloop-FastJump` | 56 | 73 | 8 | 22 | 12 MB | +| `lgpt_Luce-COMPSENTRY` | 24 | 8 | 4 | 20 | 14 MB | +| `lgpt_Float-Fi-WildDope` | 16 | 29 | 8 | 15 | 8.5 MB | +| `lgpt_djdiskmachine-SereneDream` | 16 | 24 | 7 | 21 | 2.2 MB | +| `lgpt_BA0BAB_D1BBB5` | 8 | 25 | 7 | 6 | 2.1 MB | + +`Bootloop-FastJump` is the most substantial of the five and the best place to +start. The row/chain/channel figures come from decoding the `SONG` hex blob in +each `lgptsav.dat`, which is `SONG_ROW_COUNT` x `SONG_CHANNEL_COUNT` bytes of +chain index, `0xFF` meaning empty. + +Also installed is `samplelib`, the sample library the import browser opens in. +It appears in the project list too, because the picker lists every folder in the +port directory; it is not a project and will not load. + +### One demo sample needed converting + +Two of the songs shipped with a sample stored as **32-bit IEEE float** WAV +(RIFF format code 3): `anotherworld-chord_2.wav` in `Float-Fi-WildDope` and +`01 Paint You Blue - Ingrid Schroeder_3.wav` in `Luce-COMPSENTRY`. The tracker's +WAV reader only handles 8- and 16-bit PCM, so both failed to load and left one +instrument silent in each song. + +Both were converted to 16-bit PCM at their original sample rate and channel +count, which the tracker reads correctly. That is the only modification made to +the released content. The originals remain in the release archive. + +### Installing or restoring them + +```sh +cd /storage/roms/ports/LittleGPTracker +/projects/resources/RK3566/fetch-demo-song.sh all # every demo + samplelib +/projects/resources/RK3566/fetch-demo-song.sh lgpt_Float-Fi-WildDope +``` + +The demo projects are **not** committed to this repository. They are third party +musical works, and the tracker's `.gitignore` deliberately excludes `*.wav` and +`*.dat` — upstream drops them into `projects/resources/packaging/` at packaging +time rather than committing them. Fetching them from the project's own release +archive keeps the provenance and the licensing obvious. The script also works +from a host, and honours `LGPT_RELEASE_URL` for a local copy or a mirror. + +## Display + +The tracker draws a 320x240 screen. On a 720x720 panel there is no whole +multiple that both fills the screen and fits: 2x leaves 640x480 with borders all +round, 3x would need 960 pixels of width. Whole-multiple scaling therefore makes +the text far too small to read on this hardware, which is what the rewrite of +the SDL2 window code addresses — the drawn screen is scaled to the window with +`SDL_BlitScaled` instead. + +With no `SCREENMULT` set, the screen **fills the panel**: 2.25x across and 3x +down, which makes the 8x8 font render at 18x24 physical pixels. The square panel +therefore stretches the original 4:3 layout vertically by a third. + +```xml + +``` + +restores whole-multiple, aspect-correct presentation instead: exactly 2x, +centred, 640x480, with black borders. Smaller text, undistorted proportions. +There is no setting between the two — filling is the only way to use the full +height of a square panel. + +## Theme + +The port ships the **ayu dark** theme, set through the fifteen colour keys in +`config.xml`. Every value is taken from ayu itself and mapped by the role ayu +gives it, rather than by hue matching: + +| Tracker key | ayu role | Value | +|-------------|----------|-------| +| `BACKGROUND` | `editor.background` | `10141C` | +| `FOREGROUND` | `editor.foreground` | `BFBDB6` | +| `CURSORCOLOR` | `editorCursor.foreground` | `E6B450` | +| `BORDER` | accent tint | `E6B450` | +| `ROWCOLOR1` | `editorLineNumber.activeForeground` | `5A6378` | +| `ROWCOLOR2` | `editorLineNumber.foreground` (`5a6378a6` over the background) | `404758` | +| `COL_TITLE` | `activityBarTop.foreground` | `697184` | +| `MAJORBEAT` | `activityBarTop.foreground` | `697184` | +| `SONGVIEW_00` | comment grey | `5A6673` | +| `SONGVIEW_FE` | token teal | `95E6CB` | +| `PLAYCOLOR`, `CONSOLE` | token green | `AAD94C` | +| `HICOLOR1` | token orange (keywords) | `FF8F40` | +| `HICOLOR2` | token yellow (functions) | `FFB454` | +| `MUTECOLOR` | token red | `F07178` | + +Two points are deliberate. `BACKGROUND` is ayu's *editor* surface, not its +chrome: ayu reserves the darker `0D1017` for the sidebar, status bar and panels, +and the tracker's screen is the editing canvas. And `CURSORCOLOR` shares ayu's +single gold accent with the border, because ayu itself uses one accent for the +cursor, the border and active highlights. + +Sources: [ayu-colors](https://github.com/ayu-theme/ayu-colors) and +[ayu for VS Code](https://github.com/ayu-theme/vscode-ayu), both MIT. Colours +are read once at startup, so a theme change needs a relaunch. Delete the colour +block from `config.xml` to return to the built-in purple theme. + +## Checking what the tracker draws + +The Mali GPU composites the display, so `/dev/fb0` stays blank and cannot be +used to confirm rendering. Instead the port can dump its own window surface: + +```sh +LGPT_SCREENSHOT=/tmp/lgpt.bmp ./lgpt-rk3566.elf +``` + +The surface is written as a BMP on the first redraw and then periodically. It +works under every video driver, including headless ones, which makes it useful +for checking a build without taking over the device's screen: + +```sh +SDL_VIDEODRIVER=dummy LGPT_SCREENSHOT=/tmp/lgpt.bmp timeout 5 ./lgpt-rk3566.elf +``` + +That only works when the executable is run directly. Going through +`LittleGPTracker.sh` sources `/etc/profile`, and +`/etc/profile.d/050-sway.conf` unconditionally forces +`SDL_VIDEODRIVER=wayland`, so a dummy capture has to bypass the launcher. The +same profile is what makes the launcher itself as short as it is: it supplies +`WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR` and the SDL video/audio drivers, so the +script does not need to set up the environment at all. + +## Verification performed + +* Binary needs `GLIBC_2.38` and `GLIBCXX_3.4.32`; the device provides + `GLIBC_2.38` and `GLIBCXX_3.4.33`. +* Launches on the device and stays running; the compositor reports a mapped + full-panel window (`app_id=LittleGPTracker`, 720x720). +* The joystick is detected with 4 axes, 17 buttons and 0 hats, matching the + evdev layout above, and all 13 mappings attach. +* Audio: an SDL probe requesting the exact settings the tracker uses + (44100 Hz, `AUDIO_S16SYS`, stereo, 1024 samples, via `SDL_OpenAudio`) opens + successfully as a PipeWire/PulseAudio stream on the device. +* Rendering: a headless capture of the app area is pixel-identical to an x86_64 + build of the same source, apart from the button that happened to be selected + at the time. +* Display scaling: captures at both 1024x768 (dummy driver) and 720x720 (real + Wayland session) confirm the whole panel is filled and the text is legible. + Confirmed on the device by eye as well. +* Quit hotkey: `QUITBUTTONS=8,9` parses to mask `0x300` and the tracker logs + `Quit combo mask 300` at startup. +* MIDI: the binary links `libasound.so.2`, and all 55 `snd_*` symbols it + imports are exported by the device's libasound 1.2.10. On the device the + tracker creates its ALSA sequencer clients, enumerates the ports present + (`1 input port(s) / Midi Through:0`, `1 output port(s) / Midi Through:0`), + opens the input because `config.xml` says `MIDICTRLDEVICE=*` — + `[MIDI] Controlling activated for MIDI interface Midi Through:0` — and + attaches the MIDI mapping (`[MAPPING] Attached /event/down to + midi:all:0:note:60`). +* MIDI **input**, end to end: a note 60 sent with `aplaymidi` through the + kernel's `Midi Through` loopback moved the song grid's cursor from row `00` + to row `09`. Nine rows is the expected number for a note held for one second: + the first press plus eight repeats at `KEYDELAY=200`/`KEYREPEAT=100`, which is + exactly what holding the D-pad down does. Captured before and after. +* MIDI **output**, end to end: with the same note mapped to `/event/start`, + playback began, the tracker logged `[MidiService] midi device Midi Through:0 + started`, its `RtMidi Output Client` appeared in the sequencer subscribed to + `14:0`, and `aseqdump` on the loopback captured `Start` followed by 238 + `Clock` messages while it played. +* The desktop `X64` target still builds with the shared-adapter changes. +* Demo songs: all five auto-load on the device with zero load errors and the + expected sample counts (6, 28, 15, 21, 21). `Bootloop-FastJump` was played + through to confirm audio: the log shows the project opening, its samples being + read, and `[AUDIO] pulseaudio successfully opened with 1024 samples 44100` once + playback starts, with a capture showing the song grid titled + `Song: Bootloop-FastJump` and the play position advancing. +* Theme: a capture of the song screen was analysed pixel by pixel. Every + specified ayu value is present in the rendered image, and no pixel remains from + either the built-in purple palette or an earlier incorrect mapping. + +## Troubleshooting + +* **EmulationStation ends up sharing the screen.** ROCKNIX keeps ES running and + expects whatever it launches to take the screen; sway tiles any window that + does not ask to be fullscreen. If ES is left tiled beside another app, move the + other one out of the way and ES fills the panel again: + + ```sh + SWAYSOCK=/var/run/0-runtime-dir/sway-ipc.0.sock \ + swaymsg '[app_id="gmu.bin"] move to workspace 2' + ``` + + This was observed with the built-in Music Player (`gmu.bin`), which does not + create a fullscreen window. It is unrelated to the tracker. +* **Cannot write to the deployed binary.** `scp` fails with `dest open ... + Failure` while the tracker is running, because the kernel refuses to write a + busy executable. Quit the tracker first (Select+Start). + +## Known limitations + +* ffmpeg based sample import (`PrintFX`) is not compiled in. +* MIDI interfaces are enumerated once, at startup. One plugged in later is not + picked up until the tracker is restarted, and an output device added later is + not offered in the project view until the project is reloaded. +* No MIDI interface other than the kernel's own `Midi Through` loopback was + available to test with, and the device's USB-C port is configured as a USB + device rather than a host (see "The USB port"), so the USB half of the story + is verified only down to the ALSA sequencer. +* Filling a square panel stretches the 4:3 layout vertically; there is no + scaling setting that both fills the screen and preserves the aspect ratio. +* The rendering comparison against an x86_64 build was made before the display + rewrite. The scaling itself was verified by capture and by eye, not by + byte-comparison against a reference. +* Verified on a Powkiddy RGB30 only. Other RK3566 ROCKNIX devices that use the + same `retrogame_joypad` driver should behave identically, but the button + indices in `mapping.xml` have not been confirmed on them. diff --git a/projects/Makefile b/projects/Makefile index 9fc7e43d..721bbeb2 100644 --- a/projects/Makefile +++ b/projects/Makefile @@ -108,6 +108,7 @@ BEAGLEBOARDDIRS := $(LINUXDIRS) $(DUMMYMIDIDIRS) $(RTAUDIODIRS) $(SDLDIRS) DEBDIRS := $(DBDIRS) $(JACKDIRS) $(RTAUDIODIRS) $(RTMIDIDIRS) $(SDLDIRS) RASPIDIRS := $(DBDIRS) $(RTAUDIODIRS) $(RTMIDIDIRS) $(SDLDIRS) RG35XXPLUSDIRS := $(LINUXDIRS) $(DUMMYMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) +RK3566DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) STEAMDIRS := $(LINUXDIRS) $(JACKDIRS) $(RTAUDIODIRS) $(RTMIDIDIRS) $(SDLDIRS) X64DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) X86DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) @@ -327,6 +328,7 @@ DEBFILES := $(DBFILES) $(RTAUDIOFILES) $(RTMIDIFILES) $(JACKFILES) BEAGLEBOARDFILES := $(LINUXFILES) $(DUMMYMIDIFILES) RASPIFILES := $(DBFILES) $(RTAUDIOFILES) $(RTMIDIFILES) RG35XXPLUSFILES := $(LINUXFILES) $(SDLAUDIOFILES) $(DUMMYMIDIFILES) +RK3566FILES := $(LINUXFILES) $(SDLAUDIOFILES) $(RTMIDIFILES) STEAMFILES := $(LINUXFILES) $(RTAUDIOFILES) $(RTMIDIFILES) $(JACKFILES) X64FILES := $(LINUXFILES) $(RTMIDIFILES) $(SDLAUDIOFILES) X86FILES := $(LINUXFILES) $(RTMIDIFILES) $(SDLAUDIOFILES) diff --git a/projects/Makefile.RK3566 b/projects/Makefile.RK3566 new file mode 100644 index 00000000..06a35674 --- /dev/null +++ b/projects/Makefile.RK3566 @@ -0,0 +1,107 @@ +-include $(PWD)/rules_base + +#--------------------------------------------------------------------------------- +# LittleGPTracker - RK3566 / ROCKNIX port +# +# Targets RK3566 based handhelds running ROCKNIX (Powkiddy RGB30, Anbernic +# RG-ARC, Gameforce Chi, ...). Cross-compiles on an x86_64 host but links +# against the *target's own* SDL2, so the only artifact that has to be copied +# to the device is the executable - no bundled libraries, no bundled fonts, +# no bundled libc. +# +# Requirements on the build host: +# * Arm GNU Toolchain aarch64-none-linux-gnu with glibc 2.38, to match the +# glibc shipped in ROCKNIX (GLIBC_2.38 is the newest versioned symbol +# the device exports; anything newer will not load). +# * SDL2 headers matching the device's runtime (SDL 2.30.x) plus a linker +# stub for the device's libSDL2.so. +# +# Both are located with RK3566_TOOLCHAIN / RK3566_SYSROOT and can be pointed +# somewhere else from the command line: +# +# make PLATFORM=RK3566 RK3566_SYSROOT=/opt/rocknix-sysroot +# +# See docs/RK3566_PORT.md for how the sysroot is assembled. +#--------------------------------------------------------------------------------- + +RK3566_TOOLCHAIN ?= /devel/src/.rk3566-port/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu +RK3566_SYSROOT ?= /devel/src/.rk3566-port/sysroot + +PREFIX := $(RK3566_TOOLCHAIN)/bin/aarch64-none-linux-gnu- +CROSS_COMPILE := $(PREFIX) +TOOLPATH := $(RK3566_TOOLCHAIN)/bin + +CC := $(CROSS_COMPILE)gcc +CXX := $(CROSS_COMPILE)g++ +STRIP := $(CROSS_COMPILE)strip + +#--------------------------------------------------------------------------------- +# config +# +# _64BIT : target is LP64, the tracker's default is 32bit assumptions +# CPP_MEMORY : use the C++ allocator instead of the platform allocator +# SDL2 : use the SDL2 GUI/timer adapters rather than the SDL1.2 ones +# SDLAUDIO : audio out through SDL2 (which goes to ALSA on ROCKNIX) +# RTMIDI : MIDI through the bundled RtMidi +# __LINUX_ALSA__ / __LINUX_ALSASEQ__ : select RtMidi's ALSA sequencer backend. +# RtMidi 1.0.8 actually guards that code with the ALSASEQ +# spelling, while the ALSA spelling is what the rest of the +# desktop makefiles pass; both are defined so this target +# matches X64/RASPI. ALSA seq is how the kernel presents USB +# MIDI interfaces to userspace. +# _NO_JACK_ : do not probe for a JACK server +# +# FFMPEG_ENABLED is deliberately NOT set: it only gates the ffmpeg based +# sample import in InstrumentView and would add a large dependency for a +# feature that needs a keyboard and a file dialog to be useful. +#--------------------------------------------------------------------------------- + +DEFINES := \ + -DPLATFORM_$(PLATFORM) \ + -D_64BIT \ + -DBUFFERED \ + -DCPP_MEMORY \ + -DHAVE_STDINT_H \ + -D_NDEBUG \ + -D_NO_JACK_ \ + -DSDL2 \ + -DSDLAUDIO \ + -DRTMIDI \ + -D__LINUX_ALSA__ \ + -D__LINUX_ALSASEQ__ + +# The tree mixes (SDL2 adapters) and (legacy SDL1.2 +# adapters), so both the SDL2 directory itself and its parent have to be on the +# include path. A "SDL" symlink next to the real "SDL2" directory keeps the few +# legacy includes that are still reachable in this configuration resolvable. +SDL_CFLAGS := -I$(RK3566_SYSROOT)/usr/include/SDL2 -I$(RK3566_SYSROOT)/usr/include -D_REENTRANT +SDL_LIBS := -lSDL2 + +# RtMidi's ALSA backend talks to /dev/snd/seq through libasound. The sysroot +# carries the device's own libasound.so.2 plus matching headers, so MIDI needs +# no extra runtime dependency on the device. +ALSA_LIBS := -lasound + +#--------------------------------------------------------------------------------- +# optimization +# +# RK3566 is a quad Cortex-A55. -fno-strict-aliasing is kept because the +# tracker predates strict aliasing rules and type-punning through the mixer +# would otherwise be miscompiled at -O3. +#--------------------------------------------------------------------------------- + +OPT_FLAGS := -O3 -mcpu=cortex-a55 -mtune=cortex-a55 -fno-strict-aliasing + +INCLUDES := $(SDL_CFLAGS) -I$(PWD)/../sources +CFLAGS := $(DEFINES) $(INCLUDES) $(OPT_FLAGS) -Wall +CXXFLAGS := $(CFLAGS) -std=gnu++03 +LIBS := -L$(RK3566_SYSROOT)/usr/lib \ + -Wl,--allow-shlib-undefined \ + -Wl,-rpath-link,$(RK3566_SYSROOT)/usr/lib \ + $(SDL_LIBS) $(ALSA_LIBS) -lpthread -lm -ldl + +OUTPUT = ../lgpt-rk3566 +EXTENSION := elf + +%.elf: $(OFILES) + $(CXX) $(LDFLAGS) -o $@ $(OFILES) $(LIBS) diff --git a/projects/resources/RK3566/LittleGPTracker.sh b/projects/resources/RK3566/LittleGPTracker.sh new file mode 100755 index 00000000..3911fe77 --- /dev/null +++ b/projects/resources/RK3566/LittleGPTracker.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# LittleGPTracker / Little Piggy Tracker - ROCKNIX (RK3566) launch script +# +# This is the script EmulationStation shows in the "Ports" menu. Everything +# the tracker needs - the binary, config.xml and mapping.xml - sits next to it +# in this directory, and the tracker keeps its projects and samples here too. +# +# Ported by cross-compiling against the device's own SDL2; see +# docs/RK3566_PORT.md in the source tree for how to rebuild it. + +# ROCKNIX's /etc/profile.d/050-sway.conf already exports WAYLAND_DISPLAY, +# XDG_RUNTIME_DIR, SDL_VIDEODRIVER=wayland and SDL_AUDIODRIVER=pulseaudio, +# which is exactly the environment the tracker needs. Do not second-guess it +# here: anything exported before this line gets overwritten anyway. +. /etc/profile + +# Resolve this script's directory and work from there: the tracker treats the +# current directory as "root:", where projects and samples are read from and +# written to. HOME is left alone, so PulseAudio and SDL keep their state in +# /storage rather than scattering dotfiles through the port directory. +progdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +cd "$progdir" || exit 1 + +# The tracker logs to stdout; capture it beside the binary for troubleshooting. +rm -f log.txt +./lgpt-rk3566.elf > log.txt 2>&1 + +# The tracker writes projects to the SD card; make sure they land before +# EmulationStation takes the screen back. +sync diff --git a/projects/resources/RK3566/config.xml b/projects/resources/RK3566/config.xml new file mode 100644 index 00000000..4a584c44 --- /dev/null +++ b/projects/resources/RK3566/config.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/resources/RK3566/fetch-demo-song.sh b/projects/resources/RK3566/fetch-demo-song.sh new file mode 100755 index 00000000..4867ac40 --- /dev/null +++ b/projects/resources/RK3566/fetch-demo-song.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Fetch a demo song and install it into the port directory. +# +# The demo songs ship inside the official LittleGPTracker release archives, +# alongside samplelib. They are not committed to this repository: the demo +# projects are third party musical works, and the tracker's .gitignore +# deliberately excludes *.wav and *.dat (upstream drops them into +# projects/resources/packaging/ at packaging time instead of committing them). +# Downloading them from the project's own release keeps the provenance obvious. +# +# Usage: +# ./fetch-demo-song.sh [PROJECT] [DEST] [TAG] +# +# PROJECT which demo to install (default: lgpt_Bootloop-FastJump) +# DEST port directory on the device (default: /storage/roms/ports/LittleGPTracker) +# TAG release to take it from (default: 1.6.0-bacon20) +# +# Available demo songs in the 1.6.0-bacon20 release, with the size of the song +# grid and how much of it is used (8 channels): +# +# lgpt_Bootloop-FastJump 56 rows, 73 chains, 8 channels 12 MB +# lgpt_Luce-COMPSENTRY 24 rows, 8 chains, 4 channels 14 MB +# lgpt_Float-Fi-WildDope 16 rows, 29 chains, 8 channels 8.5 MB +# lgpt_djdiskmachine-SereneDream 16 rows, 24 chains, 7 channels 2.2 MB +# lgpt_BA0BAB_D1BBB5 8 rows, 25 chains, 7 channels 2.1 MB +# +# Run this on the device (it has curl and unzip), or on a host with the port +# directory mounted. The whole release archive is ~78 MB, most of it samplelib, +# so the download is the slow part. +# +set -euo pipefail + +PROJECT="${1:-lgpt_Bootloop-FastJump}" +DEST="${2:-/storage/roms/ports/LittleGPTracker}" +TAG="${3:-1.6.0-bacon20}" + +URL="${LGPT_RELEASE_URL:-https://github.com/djdiskmachine/LittleGPTracker/releases/download/${TAG}/LGPT-X64-${TAG}.zip}" + +if [ ! -d "$DEST" ]; then + echo "port directory not found: $DEST" >&2 + exit 1 +fi + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +echo "==> downloading $TAG (about 78 MB, this takes a while)" +curl -fSL --progress-bar -o "$WORK/lgpt.zip" "$URL" + +echo "==> extracting $PROJECT" +if ! unzip -l "$WORK/lgpt.zip" "$PROJECT/*" >/dev/null 2>&1; then + echo "no such demo project in $TAG: $PROJECT" >&2 + echo "available:" >&2 + unzip -l "$WORK/lgpt.zip" | awk '{print $4}' | grep -oE '^lgpt_[^/]+' | sort -u >&2 + exit 1 +fi +unzip -q -o "$WORK/lgpt.zip" "$PROJECT/*" -d "$WORK" + +echo "==> installing into $DEST" +rm -rf "${DEST:?}/$PROJECT" +cp -r "$WORK/$PROJECT" "$DEST/$PROJECT" + +# Point the tracker at it, so enabling AUTO_LOAD_LAST later opens the demo +# rather than nothing. +printf '%s\n' "$DEST/$PROJECT" > "$DEST/last_project" + +echo +echo "installed $(du -sh "$DEST/$PROJECT" | cut -f1):" +ls "$DEST/$PROJECT" +echo +echo "Start the tracker and pick '${PROJECT#lgpt_}' from the project list." diff --git a/projects/resources/RK3566/lgpt.png b/projects/resources/RK3566/lgpt.png new file mode 100644 index 00000000..5183d51a Binary files /dev/null and b/projects/resources/RK3566/lgpt.png differ diff --git a/projects/resources/RK3566/mapping.xml b/projects/resources/RK3566/mapping.xml new file mode 100644 index 00000000..18dd74d5 --- /dev/null +++ b/projects/resources/RK3566/mapping.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/resources/RK3566/setup-sysroot.sh b/projects/resources/RK3566/setup-sysroot.sh new file mode 100755 index 00000000..b095cc21 --- /dev/null +++ b/projects/resources/RK3566/setup-sysroot.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# +# Assemble the cross-compilation sysroot used to build the RK3566 port. +# +# The port links against the *target's own* SDL2 and ALSA rather than bundled +# copies, so the sysroot only needs: +# +# 1. SDL2 and ALSA headers, at the versions the device actually ships, and +# 2. the device's libSDL2.so and libasound.so for the linker to resolve +# symbols against. +# +# Nothing else is copied: the C library comes from the Arm GNU Toolchain, whose +# glibc must match (or be older than) the glibc on the device. +# +# ALSA is what carries MIDI: RtMidi talks to /dev/snd/seq through libasound. +# +# Usage: +# ./setup-sysroot.sh [DEST] [DEVICE] +# +# DEST where to build the sysroot (default: ../sysroot) +# DEVICE ssh target of the device (default: root@RK3566.lan) +# +# Extra ssh/scp options can be passed through SSH_OPTS, which is useful when the +# build host carries a stale host key or a host specific ssh_config: +# +# SSH_OPTS="-F /dev/null -o StrictHostKeyChecking=no" ./setup-sysroot.sh +# +# Example: +# ./setup-sysroot.sh /opt/rocknix-sysroot root@192.168.1.136 +# +# Then build with: +# make PLATFORM=RK3566 RK3566_SYSROOT=/opt/rocknix-sysroot \ +# RK3566_TOOLCHAIN=/opt/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu +# +set -euo pipefail + +DEST="${1:-$(cd "$(dirname "$0")/../.." && pwd)/sysroot}" +DEVICE="${2:-root@RK3566.lan}" +SSH_OPTS="${SSH_OPTS:-}" + +# SDL2 version to fetch headers for. ROCKNIX 20240815 ships 2.30.5; using the +# matching headers avoids compiling against API that the device does not have. +SDL_VERSION="${SDL_VERSION:-2.30.5}" + +mkdir -p "$DEST/usr/include" "$DEST/usr/lib" "$DEST/dl" + +echo "==> sysroot: $DEST" +echo "==> device: $DEVICE" + +# ---------------------------------------------------------------- SDL2 headers +if [ ! -f "$DEST/usr/include/SDL2/SDL.h" ]; then + echo "==> fetching SDL2 $SDL_VERSION headers" + curl -fsSL -o "$DEST/dl/SDL2-$SDL_VERSION.tar.gz" \ + "https://libsdl.org/release/SDL2-$SDL_VERSION.tar.gz" + tar xzf "$DEST/dl/SDL2-$SDL_VERSION.tar.gz" -C "$DEST/dl" + cp -r "$DEST/dl/SDL2-$SDL_VERSION/include" "$DEST/usr/include/SDL2" + + # The tracker mixes and legacy includes, so the + # parent directory has to be on the include path and the two names must + # both resolve. + ln -sfn SDL2 "$DEST/usr/include/SDL" +else + echo "==> SDL2 headers already present, skipping" +fi + +# ------------------------------------------------- target's libSDL2 (linker) +if [ ! -f "$DEST/usr/lib/libSDL2-2.0.so.0" ]; then + echo "==> copying libSDL2 from $DEVICE" + # Resolve the real file behind the symlink, wherever the distro put it. + REMOTE_LIB=$(ssh $SSH_OPTS "$DEVICE" 'readlink -f /usr/lib/libSDL2-2.0.so.0 || readlink -f /usr/lib/libSDL2.so.0') + scp $SSH_OPTS "$DEVICE:$REMOTE_LIB" "$DEST/usr/lib/" + ln -sfn "$(basename "$REMOTE_LIB")" "$DEST/usr/lib/libSDL2.so" + ln -sfn "$(basename "$REMOTE_LIB")" "$DEST/usr/lib/libSDL2-2.0.so.0" +else + echo "==> libSDL2 already present, skipping" +fi + +# --------------------------------------------------------------- ALSA headers +# RtMidi's ALSA backend includes , which a runtime image does +# not ship: it has libasound.so.2 and no headers. The headers are taken from +# the alsa-lib release matching the device's library rather than from the build +# host, so nothing is compiled against API the device's libasound lacks. The +# release is detected from the version string the library carries. +if [ ! -f "$DEST/usr/include/alsa/asoundlib.h" ]; then + if [ -z "${ALSA_VERSION:-}" ]; then + ALSA_VERSION=$(ssh $SSH_OPTS "$DEVICE" \ + 'strings /usr/lib/libasound.so.2 | grep -E "^[0-9]+\.[0-9]+\.[0-9]+$" | sort -V | tail -1') + fi + if [ -z "$ALSA_VERSION" ]; then + echo "!! could not determine the device's alsa-lib version; set ALSA_VERSION" >&2 + exit 1 + fi + echo "==> fetching alsa-lib $ALSA_VERSION headers" + curl -fsSL -o "$DEST/dl/alsa-lib-$ALSA_VERSION.tar.bz2" \ + "https://www.alsa-project.org/files/pub/lib/alsa-lib-$ALSA_VERSION.tar.bz2" + tar xjf "$DEST/dl/alsa-lib-$ALSA_VERSION.tar.bz2" -C "$DEST/dl" + + # The tarball keeps the headers flat in include/ with a sound/ subdirectory; + # make install is what moves them under alsa/, so do that by hand. + mkdir -p "$DEST/usr/include/alsa" + cp "$DEST/dl/alsa-lib-$ALSA_VERSION"/include/*.h "$DEST/usr/include/alsa/" + cp -r "$DEST/dl/alsa-lib-$ALSA_VERSION/include/sound" "$DEST/usr/include/alsa/" +else + echo "==> ALSA headers already present, skipping" +fi + +# ---------------------------------------------- target's libasound (linker) +if [ ! -f "$DEST/usr/lib/libasound.so.2.0.0" ]; then + echo "==> copying libasound from $DEVICE" + REMOTE_LIB=$(ssh $SSH_OPTS "$DEVICE" 'readlink -f /usr/lib/libasound.so.2') + scp $SSH_OPTS "$DEVICE:$REMOTE_LIB" "$DEST/usr/lib/" + ln -sfn "$(basename "$REMOTE_LIB")" "$DEST/usr/lib/libasound.so" + ln -sfn "$(basename "$REMOTE_LIB")" "$DEST/usr/lib/libasound.so.2" +else + echo "==> libasound already present, skipping" +fi + +echo +echo "sysroot ready:" +ls -l "$DEST/usr/lib/libSDL2"* "$DEST/usr/lib/libasound"* +echo +echo "Check that the device glibc is not newer than the toolchain's:" +echo " device: ssh $DEVICE 'ldd --version | head -1'" +echo " toolchain: \$TOOLCHAIN/bin/aarch64-none-linux-gnu-gcc -print-sysroot" diff --git a/projects/resources/packaging/lgpt_package.sh b/projects/resources/packaging/lgpt_package.sh index 968f7731..931cb631 100755 --- a/projects/resources/packaging/lgpt_package.sh +++ b/projects/resources/packaging/lgpt_package.sh @@ -22,6 +22,7 @@ collect_resources() { #1PLATFORM #2lgpt.*-exe if [ "$1" == "PSP" ] || [ "$1" == "GARLIC" ] || [ "$1" == "RG35XXPLUS" ] || + [ "$1" == "RK3566" ] || [ "$1" == "BITTBOY" ]; then # All files go in the root folder zip -9 $PACKAGE -j $CONTENTS elif [ "$1" == "MACOS" ]; then # .app is a folder @@ -54,6 +55,7 @@ collect_resources CHIP lgpt.chip-exe collect_resources BITTBOY lgpt-bittboy.elf collect_resources GARLICPLUS lgpt-garlicplus.elf collect_resources RG35XXPLUS lgpt-rg35xxplus.elf +collect_resources RK3566 lgpt-rk3566.elf collect_resources MACOS LittleGPTracker.app # collect_resources RS97 lgpt.dge # collect_resources STEAM lgpt.steam-exe diff --git a/sources/Adapters/RTMidi/RTMidiInDevice.cpp b/sources/Adapters/RTMidi/RTMidiInDevice.cpp index 36f67d83..55411389 100644 --- a/sources/Adapters/RTMidi/RTMidiInDevice.cpp +++ b/sources/Adapters/RTMidi/RTMidiInDevice.cpp @@ -1,5 +1,6 @@ #include "RTMidiInDevice.h" #include "Services/Midi/MidiMessage.h" +#include "System/Console/Trace.h" #include "System/Console/n_assert.h" void mycallback( double deltatime, std::vector< unsigned char > *message, void *userData ) @@ -26,7 +27,17 @@ RTMidiInDevice::~RTMidiInDevice() { bool RTMidiInDevice::initDriver() { - rtMidiIn_.openPort(index_) ; + // RtMidi signals failure by throwing, and the port list this index came + // from was built earlier: the interface may have been unplugged since, or + // something else may be holding the port. An interface that cannot be + // opened is not fatal, the tracker runs without it. + try { + rtMidiIn_.openPort(index_) ; + } catch (RtError &error) { + Trace::Log("RTMidiInDevice","Could not open %s: %s",GetName(),error.getMessageString()) ; + return false ; + } + rtMidiIn_.setCallback( &mycallback,this ); // ignore sysex, timing, or active sensing messages. @@ -36,7 +47,11 @@ bool RTMidiInDevice::initDriver() { } ; void RTMidiInDevice::closeDriver() { - rtMidiIn_.closePort() ; + try { + rtMidiIn_.closePort() ; + } catch (RtError &error) { + Trace::Log("RTMidiInDevice","Could not close %s: %s",GetName(),error.getMessageString()) ; + } } ; bool RTMidiInDevice::startDriver() { diff --git a/sources/Adapters/RTMidi/RTMidiOutDevice.cpp b/sources/Adapters/RTMidi/RTMidiOutDevice.cpp index 328b27ab..98c07678 100644 --- a/sources/Adapters/RTMidi/RTMidiOutDevice.cpp +++ b/sources/Adapters/RTMidi/RTMidiOutDevice.cpp @@ -22,14 +22,27 @@ void RTMidiOutDevice::Close(){ } ; bool RTMidiOutDevice::Start(){ - rtMidiOut_.openPort( index_ ); + // RtMidi signals failure by throwing, and the port list this index came + // from was built when the project was opened: the interface may have been + // unplugged since. Failing to open is reported to the caller rather than + // taking the tracker down with it. + try { + rtMidiOut_.openPort( index_ ); + } catch (RtError &error) { + Trace::Log("RTMidiOutDevice","Could not open %s: %s",GetName(),error.getMessageString()) ; + return false ; + } running_=true ; return true ; } ; void RTMidiOutDevice::Stop(){ running_=false ; - rtMidiOut_.closePort() ; + try { + rtMidiOut_.closePort() ; + } catch (RtError &error) { + Trace::Log("RTMidiOutDevice","Could not close %s: %s",GetName(),error.getMessageString()) ; + } } ; void RTMidiOutDevice::SendMessage(MidiMessage &msg) @@ -48,6 +61,14 @@ void RTMidiOutDevice::SendMessage(MidiMessage &msg) { message.push_back(msg.data2_) ; } - rtMidiOut_.sendMessage( &message ); + + // Same reason as Start(): an interface unplugged mid-song must not take + // the audio thread down with it. + try { + rtMidiOut_.sendMessage( &message ); + } catch (RtError &error) { + Trace::Log("RTMidiOutDevice","Could not send to %s: %s",GetName(),error.getMessageString()) ; + running_=false ; + } } } diff --git a/sources/Adapters/SDL2/Audio/SDLAudioDriver.cpp b/sources/Adapters/SDL2/Audio/SDLAudioDriver.cpp index 7d3c2552..7ac86dc1 100644 --- a/sources/Adapters/SDL2/Audio/SDLAudioDriver.cpp +++ b/sources/Adapters/SDL2/Audio/SDLAudioDriver.cpp @@ -3,6 +3,7 @@ #include "Services/Time/TimeService.h" #include "System/Console/Trace.h" #include "System/System/System.h" +#include void sdl_callback(void *userdata, Uint8 *stream, int len) { SDLAudioDriver *sound = (SDLAudioDriver *)userdata; diff --git a/sources/Adapters/SDL2/GUI/SDLEventManager.cpp b/sources/Adapters/SDL2/GUI/SDLEventManager.cpp index 0c035b7e..48f704cd 100644 --- a/sources/Adapters/SDL2/GUI/SDLEventManager.cpp +++ b/sources/Adapters/SDL2/GUI/SDLEventManager.cpp @@ -10,6 +10,40 @@ bool SDLEventManager::dumpEvent_=false ; SDLEventManager::SDLEventManager() { + quitButtonMask_=0 ; + buttonsDown_=0 ; +} + +// Buttons that quit the tracker when held together, from the QUITBUTTONS +// config value (comma separated SDL joystick button indices, eg "8,9" for +// Select+Start). Leave the value out to disable the combo. +void SDLEventManager::LoadQuitCombo() { + + quitButtonMask_=0 ; + const char *value=Config::GetInstance()->GetValue("QUITBUTTONS") ; + if (!value) return ; + + const char *p=value ; + while (*p) { + int button=atoi(p) ; + if ((button>=0)&&(button<32)) { + quitButtonMask_|=(1u<MAX_JOY_COUNT)?MAX_JOY_COUNT:joyCount ; keyboardCS_=new KeyboardControllerSource("keyboard") ; + LoadQuitCombo() ; const char *dumpIt=Config::GetInstance()->GetValue("DUMPEVENT") ; if ((dumpIt)&&(!strcmp(dumpIt,"YES"))) { @@ -95,21 +130,51 @@ int SDLEventManager::MainLoop() case SDL_JOYBUTTONDOWN: - buttonCS_[event.jbutton.which]->SetButton(event.jbutton.button,true) ; + { + int joy=JoystickIndexForInstance(event.jbutton.which) ; + if (joy>=0) { + buttonCS_[joy]->SetButton(event.jbutton.button,true) ; + } + if (quitButtonMask_&&(event.jbutton.button<32)) + { + buttonsDown_|=(1u<ProcessQuit() ; + } + } + } break ; case SDL_JOYBUTTONUP: + { if (dumpEvent_) { Trace::Log("EVENT","but(%d):%d",event.button.which,event.jbutton.button) ; } - buttonCS_[event.jbutton.which]->SetButton(event.jbutton.button,false) ; + int joy=JoystickIndexForInstance(event.jbutton.which) ; + if (joy>=0) { + buttonCS_[joy]->SetButton(event.jbutton.button,false) ; + } + if (event.jbutton.button<32) + { + buttonsDown_&=~(1u<SetAxis(event.jaxis.axis,float(event.jaxis.value)/32767.0f) ; + int joy=JoystickIndexForInstance(event.jaxis.which) ; + if (joy>=0) { + joystickCS_[joy]->SetAxis(event.jaxis.axis,float(event.jaxis.value)/32767.0f) ; + } + } break ; case SDL_JOYHATMOTION: + { if (dumpEvent_) { for (int i=0;i<4;i++) @@ -121,7 +186,11 @@ int SDLEventManager::MainLoop() } } } - hatCS_[event.jhat.which]->SetHat(event.jhat.hat,event.jhat.value) ; + int joy=JoystickIndexForInstance(event.jhat.which) ; + if (joy>=0) { + hatCS_[joy]->SetHat(event.jhat.hat,event.jhat.value) ; + } + } break ; case SDL_JOYBALLMOTION: if (dumpEvent_) diff --git a/sources/Adapters/SDL2/GUI/SDLEventManager.h b/sources/Adapters/SDL2/GUI/SDLEventManager.h index 6ed26991..6c2c0f15 100644 --- a/sources/Adapters/SDL2/GUI/SDLEventManager.h +++ b/sources/Adapters/SDL2/GUI/SDLEventManager.h @@ -31,5 +31,16 @@ class SDLEventManager: public T_Singleton,public EventManager { JoystickControllerSource *joystickCS_[MAX_JOY_COUNT] ; HatControllerSource *hatCS_[MAX_JOY_COUNT] ; KeyboardControllerSource *keyboardCS_ ; + + // Buttons that, held together, quit the tracker. Handhelds have no window + // manager to close the window and the tracker's own quit control lives in + // the project picker, which is unreachable once a project is loaded, so + // without this there is no way out of a running project. + unsigned int quitButtonMask_ ; + unsigned int buttonsDown_ ; + void LoadQuitCombo() ; + // SDL reports a joystick *instance id* in events, which is not the same as + // the index the joysticks were opened with. Map one to the other, or -1. + int JoystickIndexForInstance(SDL_JoystickID instanceId) ; } ; #endif diff --git a/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp b/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp index 7f4eb05a..55d745d1 100644 --- a/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp +++ b/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp @@ -1,5 +1,6 @@ #include "SDLGUIWindowImp.h" #include +#include #include #include "Application/Model/Config.h" #include "Application/Utils/char.h" @@ -15,12 +16,32 @@ SDLGUIWindowImp *instance_ ; unsigned short appWidth=320 ; unsigned short appHeight=240 ; +// Opt-in rendering diagnostic: set LGPT_SCREENSHOT to a path and the presented +// window is written there as a BMP, which is the only way to confirm what the +// tracker actually draws on a device whose framebuffer is composited by the GPU +// (and therefore unreadable through /dev/fb0). +static const char *screenshotPath_=0 ; +static Uint32 screenshotLast_=0 ; +// Minimum gap between captures. Flush() is only called when the UI +// actually changes, so a fixed frame count either fires too early (on +// the loading screen) or never, depending on the video driver. +#define SCREENSHOT_INTERVAL_MS 400 + SDLGUIWindowImp::SDLGUIWindowImp(GUICreateWindowParams &p) { SDLCreateWindowParams &sdlP=(SDLCreateWindowParams &)p; cacheFonts_=sdlP.cacheFonts_ ; framebuffer_=sdlP.framebuffer_ ; + + if (!screenshotPath_) + { + const char *shot=getenv("LGPT_SCREENSHOT") ; + if ((shot)&&(shot[0])) + { + screenshotPath_=shot ; + } + } // By default if we are not running a framebuffer device // we assumed it's windowed @@ -77,49 +98,61 @@ SDLGUIWindowImp::SDLGUIWindowImp(GUICreateWindowParams &p) framebuffer_ = true; windowed_ = false; } - - #ifdef PLATFORM_PSP - mult_ = 1; - #else - int multFromSize=MIN(screenHeight/appHeight,screenWidth/appWidth); - const char *mult=Config::GetInstance()->GetValue("SCREENMULT") ; - if (mult) - { - mult_=atoi(mult); - } - else - { - if (framebuffer_) - { - mult_ = multFromSize; - } - else - { - mult_ = 1; - } - } - #endif - // Create a window that is the requested size - - screenRect_._topLeft._x=0; - screenRect_._topLeft._y=0; - screenRect_._bottomRight._x=windowed_?appWidth*mult_:screenWidth; - screenRect_._bottomRight._y=windowed_?appHeight*mult_:screenHeight; - Trace::Log("DISPLAY","Creating SDL Window (%d,%d)",screenRect_.Width(), screenRect_.Height()); + // A fullscreen window covers the whole display, so the app area has to be + // scaled up into it rather than being the window itself. Without this the + // tracker renders a window sized to the app area and leaves the rest of a + // high resolution panel (eg. the 720x720 RGB30 screen) untouched. + if (fullscreen) + { + windowed_ = false; + } + + // Drawing always happens at 1:1 into a fixed 320x240 surface and Flush() + // scales that onto the window, so SCREENMULT is a presentation scale rather + // than a drawing scale. Leaving it unset fills the window, which is what + // makes the UI usable on a 720x720 panel: eight pixel glyphs at 2x are + // unreadably small there, at 2.25x3 they are not. + mult_ = 1; + presentMult_ = 0; + const char *mult = Config::GetInstance()->GetValue("SCREENMULT"); + if (mult) + { + presentMult_ = atoi(mult); + } + + // Fullscreen takes the whole display; windowed honours SCREENMULT. + int windowMult = (presentMult_ > 0) ? presentMult_ : 1; + int windowWidth = windowed_ ? appWidth * windowMult : screenWidth; + int windowHeight = windowed_ ? appHeight * windowMult : screenHeight; + if (windowWidth < appWidth) windowWidth = appWidth; + if (windowHeight < appHeight) windowHeight = appHeight; + + Trace::Log("DISPLAY", "Creating SDL Window (%d,%d) for a %dx%d screen", + windowWidth, windowHeight, appWidth, appHeight); window_ = SDL_CreateWindow("LittleGPTracker",SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, - screenRect_.Width(),screenRect_.Height(),fullscreen?SDL_WINDOW_FULLSCREEN:SDL_WINDOW_SHOWN); + windowWidth,windowHeight,fullscreen?SDL_WINDOW_FULLSCREEN:SDL_WINDOW_SHOWN); NAssert(window_) ; - // Compute the x & y offset to locate our app window + SDL_SetWindowIcon(window_, SDL_LoadBMP("lgpt_icon.bmp")); - appAnchorX_=(screenRect_.Width()-appWidth*mult_)/2 ; - appAnchorY_=(screenRect_.Height()-appHeight*mult_)/2 ; + SDL_Surface *windowSurface = SDL_GetWindowSurface(window_); + NAssert(windowSurface) ; - SDL_SetWindowIcon(window_, SDL_LoadBMP("lgpt_icon.bmp")); - surface_ = SDL_GetWindowSurface(window_); + // Fixed size drawing surface, in the window's pixel format so that the + // scaled blit in Flush() stays a plain format copy. + frame_ = SDL_CreateRGBSurfaceWithFormat(0, appWidth, appHeight, 32, + windowSurface->format->format); + NAssert(frame_) ; + surface_ = frame_; - NAssert(surface_) ; + // The app fills the drawing surface; presentation is handled separately. + screenRect_._topLeft._x = 0; + screenRect_._topLeft._y = 0; + screenRect_._bottomRight._x = appWidth; + screenRect_._bottomRight._y = appHeight; + appAnchorX_ = 0; + appAnchorY_ = 0; Uint32 rmask, gmask, bmask, amask; @@ -495,30 +528,67 @@ void SDLGUIWindowImp::Unlock() void SDLGUIWindowImp::Flush() { - // blit partial updates on resource constrained platforms - if ((!framebuffer_)&&(updateCount_!=0)) + // The app drew into the fixed size frame_ surface; scale that onto the + // window. SDL_BlitScaled samples nearest neighbour, which keeps the pixel + // font crisp at the non whole multiples a 720x720 panel needs. + SDL_Surface *windowSurface=SDL_GetWindowSurface(window_) ; + if (windowSurface) { - if (updateCount_0) { - SDL_UpdateWindowSurfaceRects(window_,updateRects_,updateCount_); + // SCREENMULT: a whole multiple, centred, reduced until it fits. + int s=presentMult_ ; + while ((s>1)&&((appWidth*s>windowSurface->w)||(appHeight*s>windowSurface->h))) + { + s-- ; + } + dst.w=appWidth*s ; + dst.h=appHeight*s ; } else { - SDL_Rect updateRect; - updateRect.x = screenRect_.Left(); - updateRect.y = screenRect_.Top(); - updateRect.w = screenRect_.Width(); - updateRect.h = screenRect_.Height(); - SDL_UpdateWindowSurfaceRects(window_,&updateRect,1); + // Fill the window. On a square panel this is what makes the UI + // large enough to read; the aspect ratio is stretched to match. + dst.w=windowSurface->w ; + dst.h=windowSurface->h ; } + dst.x=(windowSurface->w-dst.w)/2 ; + dst.y=(windowSurface->h-dst.h)/2 ; + + SDL_BlitScaled(frame_,0,windowSurface,&dst) ; + SDL_UpdateWindowSurface(window_) ; } updateCount_=0; + + // Write the surface out whenever the UI changes, rate limited, so that the + // file ends up holding the most recent screen rather than whichever frame + // happened to be first. + if (screenshotPath_) + { + Uint32 now=SDL_GetTicks() ; + if ((screenshotLast_==0)||((now-screenshotLast_)>=SCREENSHOT_INTERVAL_MS)) + { + screenshotLast_=now ; + // Capture what was presented rather than the app's own surface, so + // that a capture also exercises the scaling step. + SDL_Surface *shot=SDL_GetWindowSurface(window_) ; + if ((shot)&&(SDL_SaveBMP(shot,screenshotPath_)==0)) + { + Trace::Log("DISPLAY","Wrote screenshot to %s",screenshotPath_) ; + } + else + { + Trace::Error("DISPLAY","Could not write %s: %s",screenshotPath_,SDL_GetError()) ; + } + } + } } void SDLGUIWindowImp::ProcessExpose() { - // Expose and resize events will cause a new surface to be needed. - surface_ = SDL_GetWindowSurface(window_); + // The drawing surface is fixed size and independent of the window, so an + // expose or resize only means the window has to be redrawn. _window->Update() ; } diff --git a/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.h b/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.h index 956348de..9f665721 100644 --- a/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.h +++ b/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.h @@ -52,6 +52,11 @@ class SDLGUIWindowImp: public I_GUIWindowImp { SDL_Window *window_; SDL_Surface *surface_; // SDL_Surface *offscreen_ ; + // The tracker always draws a 320x240 screen into this surface at 1:1; + // Flush() scales it onto the window. Keeping drawing and presentation + // apart is what allows the UI to fill a panel that is not a whole multiple + // of 320x240 (eg. the 720x720 screen of an RGB30). + SDL_Surface *frame_; GUIRect screenRect_ ; unsigned int currentColor_ ; unsigned int backgroundColor_ ; @@ -65,5 +70,7 @@ class SDLGUIWindowImp: public I_GUIWindowImp { int appAnchorX_ ; int appAnchorY_ ; int mult_ ; + // Presentation scale taken from SCREENMULT, or 0 to scale to the window. + int presentMult_ ; } ; #endif diff --git a/sources/Application/Application.cpp b/sources/Application/Application.cpp index 9f132a15..871bc5aa 100644 --- a/sources/Application/Application.cpp +++ b/sources/Application/Application.cpp @@ -19,11 +19,21 @@ void Application::initMidiInput() { const char *preferedDevice=Config::GetInstance()->GetValue("MIDICTRLDEVICE"); + // MIDICTRLDEVICE normally names one interface, matched on a prefix because + // drivers append their own port number ("Midi Through" matches + // "Midi Through:0"). "*" means "every input that is plugged in", which is + // what a handheld wants: the name of whatever keyboard is connected is not + // known in advance and cannot be put in a config file that ships with the + // build. Interfaces that nothing is mapped to cost nothing, so opening all + // of them is harmless. + bool allDevices=(preferedDevice)&&(!strcmp(preferedDevice,"*")) ; + IteratorPtrit(MidiService::GetInstance()->GetInIterator()) ; for(it->Begin();!it->IsDone();it->Next()) { MidiInDevice &in=it->CurrentItem() ; - if ((preferedDevice) && (!strncmp(in.GetName(), preferedDevice, strlen(preferedDevice)))) + if ((preferedDevice) && + (allDevices || (!strncmp(in.GetName(), preferedDevice, strlen(preferedDevice))))) { if (in.Init()) {