From a590a209b1253eb29349e49c24be91b20bc0b2d0 Mon Sep 17 00:00:00 2001 From: Hans Baier Date: Mon, 21 Sep 2026 08:37:03 +0700 Subject: [PATCH 1/2] feat: add RK3566 / ROCKNIX port Cross-compiles LittleGPTracker for RK3566 handhelds running ROCKNIX (Powkiddy RGB30 and friends), linking against the target's own SDL2 so the deliverable is a single executable. Verified on an RGB30: it launches, fills the 720x720 panel, reads the gamepad and plays audio. Port files: - projects/Makefile.RK3566: build target, with RK3566DIRS/RK3566FILES registered in projects/Makefile and the packaging script - projects/resources/RK3566: config.xml, mapping.xml, launch script, sysroot setup and a demo-song fetcher Shared SDL2 adapter changes: - SDLGUIWindowImp: separate drawing from presentation. The 320x240 screen is drawn at 1:1 into a fixed surface and Flush() scales it onto the window with SDL_BlitScaled, so the UI can fill a panel that is not a whole multiple of the app resolution. A fullscreen window used to keep its app-sized surface, and whole-number scaling capped a 720x720 panel at 2x, which left the text unreadably small. Adds an opt-in LGPT_SCREENSHOT diagnostic. - SDLEventManager: configurable quit hotkey (QUITBUTTONS), because the tracker's only quit control is the project picker's Exit button, which cannot be reached once a project is loaded. Also map SDL joystick instance ids back to the index the device was opened with, rather than indexing four-element arrays with them directly - which overran as soon as a second controller was hotplugged. - SDLAudioDriver: include for memcpy/memmove, which newer libstdc++ headers no longer pull in transitively. --- README.md | 1 + docs/RK3566_PORT.md | 347 ++++++++++++++++++ projects/Makefile | 2 + projects/Makefile.RK3566 | 94 +++++ projects/resources/RK3566/LittleGPTracker.sh | 31 ++ projects/resources/RK3566/config.xml | 98 +++++ projects/resources/RK3566/fetch-demo-song.sh | 72 ++++ projects/resources/RK3566/lgpt.png | Bin 0 -> 11486 bytes projects/resources/RK3566/mapping.xml | 62 ++++ projects/resources/RK3566/setup-sysroot.sh | 81 ++++ projects/resources/packaging/lgpt_package.sh | 2 + .../Adapters/SDL2/Audio/SDLAudioDriver.cpp | 1 + sources/Adapters/SDL2/GUI/SDLEventManager.cpp | 77 +++- sources/Adapters/SDL2/GUI/SDLEventManager.h | 11 + sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp | 166 ++++++--- sources/Adapters/SDL2/GUI/SDLGUIWindowImp.h | 7 + 16 files changed, 1000 insertions(+), 52 deletions(-) create mode 100644 docs/RK3566_PORT.md create mode 100644 projects/Makefile.RK3566 create mode 100755 projects/resources/RK3566/LittleGPTracker.sh create mode 100644 projects/resources/RK3566/config.xml create mode 100755 projects/resources/RK3566/fetch-demo-song.sh create mode 100644 projects/resources/RK3566/lgpt.png create mode 100644 projects/resources/RK3566/mapping.xml create mode 100755 projects/resources/RK3566/setup-sysroot.sh diff --git a/README.md b/README.md index 2356147d..7d26e328 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 | NO | NO | YES | ROCKNIX handhelds (Powkiddy RGB30 etc.) [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..d59cec85 --- /dev/null +++ b/docs/RK3566_PORT.md @@ -0,0 +1,347 @@ +# 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, 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 from a device over ssh | +| `projects/resources/RK3566/fetch-demo-song.sh` | Installs a demo song from the official release | + +Four changes were needed in shared code, each marked with a comment: + +* `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 | + +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**. 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. `projects/resources/RK3566/setup-sysroot.sh` assembles 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`, `DUMMYMIDI` 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. + +## 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. +* 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 + +* MIDI is stubbed out (`DUMMYMIDI`). USB MIDI may work on ROCKNIX but is + untested here. +* ffmpeg based sample import (`PrintFX`) is not compiled in. +* 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..4d8e19c0 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) $(DUMMYMIDIDIRS) $(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) $(DUMMYMIDIFILES) 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..455d2033 --- /dev/null +++ b/projects/Makefile.RK3566 @@ -0,0 +1,94 @@ +-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) +# DUMMYMIDI : no MIDI hardware on these handhelds; keep the stub +# _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 \ + -DDUMMYMIDI + +# 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 + +#--------------------------------------------------------------------------------- +# 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) -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..11de599f --- /dev/null +++ b/projects/resources/RK3566/config.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..5183d51a79642dc9b439efb7592704285bd7a711 GIT binary patch literal 11486 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 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/resources/RK3566/setup-sysroot.sh b/projects/resources/RK3566/setup-sysroot.sh new file mode 100755 index 00000000..7c169d0e --- /dev/null +++ b/projects/resources/RK3566/setup-sysroot.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# +# Assemble the cross-compilation sysroot used to build the RK3566 port. +# +# The port links against the *target's own* SDL2 rather than a bundled copy, so +# the sysroot only needs two things: +# +# 1. SDL2 headers, at the version the device actually ships, and +# 2. the device's libSDL2.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. +# +# 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 + +echo +echo "sysroot ready:" +ls -l "$DEST/usr/lib/libSDL2"* +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/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 From 7989a43790d457c5f49234fd8311c1824cb7c642 Mon Sep 17 00:00:00 2001 From: Hans Baier Date: Mon, 21 Sep 2026 11:27:22 +0700 Subject: [PATCH 2/2] feat(RK3566): MIDI through the ALSA sequencer The port was built with DUMMYMIDI, so the tracker had a stub that enumerated nothing and opened nothing. Build the bundled RtMidi with its ALSA sequencer backend instead and link libasound: the ALSA sequencer is how the kernel presents USB MIDI interfaces (snd-usb-audio is built into ROCKNIX), and it is independent of the audio server, so PipeWire and aseqnet ports work through it too. - projects/Makefile.RK3566: RTMIDI plus __LINUX_ALSA__/__LINUX_ALSASEQ__ instead of DUMMYMIDI, and -lasound. Both spellings are defined because RtMidi 1.0.8 guards its ALSA code with the ALSASEQ one, which the desktop makefiles only get right by accident. - setup-sysroot.sh: fetch the alsa-lib headers matching the device's libasound and copy the library itself into the sysroot. A runtime image has neither. - Application::initMidiInput: accept "*" for MIDICTRLDEVICE, meaning every input present. The name of an interface has to be known in advance to be written into a config file, and on a handheld it is not; without a match the tracker opens no MIDI input at all. - RTMidiInDevice/RTMidiOutDevice: RtMidi signals failure by throwing, and the port index comes from a list built earlier, so an interface unplugged in between took the tracker down with it. Report the failure and disable that one device instead. This is the case USB MIDI makes ordinary. - config.xml: MIDICTRLDEVICE='*', documented together with how MIDI events are mapped onto tracker controls. Verified on the device over the kernel's Midi Through loopback, which is an ordinary sequencer client and so exercises the same code a USB interface would. Input: a note 60 received through the mapping moved the song grid cursor, by the press-plus-autorepeat count a held button produces. Output: playback opened the tracked output port and aseqdump captured the Start byte and 238 clock messages. The desktop X64 target still builds with the shared adapter changes. The RGB30's USB-C port is a peripheral in the device tree and ROCKNIX binds its MTP/CDC gadget to it, so a keyboard attached there will not enumerate. That is a property of the port; everything above the kernel is in place. --- README.md | 2 +- docs/RK3566_PORT.md | 157 ++++++++++++++++++-- projects/Makefile | 4 +- projects/Makefile.RK3566 | 19 ++- projects/resources/RK3566/config.xml | 28 ++++ projects/resources/RK3566/setup-sysroot.sh | 55 ++++++- sources/Adapters/RTMidi/RTMidiInDevice.cpp | 19 ++- sources/Adapters/RTMidi/RTMidiOutDevice.cpp | 27 +++- sources/Application/Application.cpp | 12 +- 9 files changed, 290 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 7d26e328..715bcc86 100644 --- a/README.md +++ b/README.md @@ -55,7 +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 | NO | NO | YES | ROCKNIX handhelds (Powkiddy RGB30 etc.) [See notes](docs/RK3566_PORT.md) | +| 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 index d59cec85..795f0327 100644 --- a/docs/RK3566_PORT.md +++ b/docs/RK3566_PORT.md @@ -11,13 +11,18 @@ no bundled libraries, no bundled fonts, no bundled C library. |------|---------| | `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, ayu dark theme | +| `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 from a device over ssh | +| `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 | -Four changes were needed in shared code, each marked with a comment: +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 @@ -49,23 +54,26 @@ Four changes were needed in shared code, each marked with a comment: | 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**. 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: +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. `projects/resources/RK3566/setup-sysroot.sh` assembles this over - ssh: + 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 @@ -82,10 +90,11 @@ make PLATFORM=RK3566 \ ``` The result is `projects/lgpt-rk3566.elf`. Build defines are `_64BIT`, -`CPP_MEMORY`, `SDL2`, `SDLAUDIO`, `DUMMYMIDI` 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. +`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 @@ -153,6 +162,100 @@ 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 @@ -304,6 +407,25 @@ script does not need to set up the environment at all. 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 @@ -334,9 +456,14 @@ script does not need to set up the environment at all. ## Known limitations -* MIDI is stubbed out (`DUMMYMIDI`). USB MIDI may work on ROCKNIX but is - untested here. * 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 diff --git a/projects/Makefile b/projects/Makefile index 4d8e19c0..721bbeb2 100644 --- a/projects/Makefile +++ b/projects/Makefile @@ -108,7 +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) $(DUMMYMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) +RK3566DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) STEAMDIRS := $(LINUXDIRS) $(JACKDIRS) $(RTAUDIODIRS) $(RTMIDIDIRS) $(SDLDIRS) X64DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) X86DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) @@ -328,7 +328,7 @@ DEBFILES := $(DBFILES) $(RTAUDIOFILES) $(RTMIDIFILES) $(JACKFILES) BEAGLEBOARDFILES := $(LINUXFILES) $(DUMMYMIDIFILES) RASPIFILES := $(DBFILES) $(RTAUDIOFILES) $(RTMIDIFILES) RG35XXPLUSFILES := $(LINUXFILES) $(SDLAUDIOFILES) $(DUMMYMIDIFILES) -RK3566FILES := $(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 index 455d2033..06a35674 100644 --- a/projects/Makefile.RK3566 +++ b/projects/Makefile.RK3566 @@ -42,7 +42,13 @@ STRIP := $(CROSS_COMPILE)strip # 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) -# DUMMYMIDI : no MIDI hardware on these handhelds; keep the stub +# 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 @@ -60,7 +66,9 @@ DEFINES := \ -D_NO_JACK_ \ -DSDL2 \ -DSDLAUDIO \ - -DDUMMYMIDI + -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 @@ -69,6 +77,11 @@ DEFINES := \ 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 # @@ -85,7 +98,7 @@ CXXFLAGS := $(CFLAGS) -std=gnu++03 LIBS := -L$(RK3566_SYSROOT)/usr/lib \ -Wl,--allow-shlib-undefined \ -Wl,-rpath-link,$(RK3566_SYSROOT)/usr/lib \ - $(SDL_LIBS) -lpthread -lm -ldl + $(SDL_LIBS) $(ALSA_LIBS) -lpthread -lm -ldl OUTPUT = ../lgpt-rk3566 EXTENSION := elf diff --git a/projects/resources/RK3566/config.xml b/projects/resources/RK3566/config.xml index 11de599f..4a584c44 100644 --- a/projects/resources/RK3566/config.xml +++ b/projects/resources/RK3566/config.xml @@ -90,6 +90,34 @@ + + +