├── .github └── workflows │ └── build.yml ├── .gitmodules ├── .hgignore ├── .hgtags ├── AUTHORS ├── COPYING ├── Dockerfile ├── Makefile.am ├── README.md ├── assets ├── hbasbadge-wiiu.png ├── papaya-banner.png ├── papaya-banner.xcf ├── papaya-icon.png └── papaya-icon.xcf ├── bootstrap ├── configure.ac ├── docker-build.sh ├── m4 ├── ax_add_am_macro.m4 ├── ax_am_macros.m4 ├── ax_append_compile_flags.m4 ├── ax_append_flag.m4 ├── ax_append_to_file.m4 ├── ax_check_compile_flag.m4 ├── ax_check_library.m4 ├── ax_file_escapes.m4 ├── ax_prepend_flag.m4 ├── ax_print_to_file.m4 ├── ax_require_defined.m4 ├── ax_subdirs_configure.m4 ├── ax_var_push.m4 ├── devkitpro.m4 ├── devkitpro_ppc.m4 ├── devkitpro_wut.m4 ├── wiiu_wums.m4 └── wiiu_wups.m4 └── src ├── Makefile.am ├── ax_mon.cpp ├── ax_mon.hpp ├── cfg.cpp ├── cfg.hpp ├── cpu_mon.cpp ├── cpu_mon.hpp ├── fs_mon.cpp ├── fs_mon.hpp ├── gx2_mon.cpp ├── gx2_mon.hpp ├── gx2_perf.h ├── main.cpp ├── net_mon.cpp ├── net_mon.hpp ├── out_span.cpp ├── out_span.hpp ├── overlay.cpp ├── overlay.hpp ├── pad_mon.cpp ├── pad_mon.hpp ├── time_mon.cpp ├── time_mon.hpp ├── utils.cpp └── utils.hpp /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build-plugin: 7 | runs-on: ubuntu-latest 8 | name: Build plugin 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 1 14 | submodules: true 15 | 16 | - name: Setup Docker 17 | run: docker build --tag plugin-image . 18 | 19 | - name: Build 20 | uses: addnab/docker-run-action@v3 21 | with: 22 | image: plugin-image 23 | options: --name plugin-container 24 | run: | 25 | ./bootstrap 26 | ./configure --host=powerpc-eabi CXXFLAGS="-Os -ffunction-sections -fipa-pta -Wno-odr -flto" AR=powerpc-eabi-gcc-ar RANLIB=powerpc-eabi-gcc-ranlib 27 | make 28 | 29 | - name: Copy from container to host 30 | uses: tj-actions/docker-cp@v2 31 | with: 32 | container: plugin-container 33 | source: /home/user/src/papaya-hud.wps 34 | destination: papaya-hud.wps 35 | 36 | - name: Upload 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: papaya-hud.wps 40 | path: "*.wps" 41 | if-no-files-found: error 42 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/libwupsxx"] 2 | path = external/libwupsxx 3 | url = https://github.com/dkosmari/libwupsxx.git 4 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.elf 4 | *.o 5 | *.tar.gz 6 | *.wps 7 | *~ 8 | .deps 9 | .dirstamp 10 | autom4te.cache 11 | build-aux 12 | config.h 13 | config.h.in 14 | config.log 15 | configure 16 | Makefile 17 | Makefile.in 18 | stamp-h1 19 | *.a 20 | aminclude.am 21 | aclocal.m4 22 | compile_flags.txt 23 | config.status 24 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 0a32199a2e3c7362be72afae4d075d97121196bd v0.1-pre 2 | 6d9f817992bcfb8c6c29b8acb93e9b3eddb55e67 v0.1 3 | 1ffdcbc8d7cc460da566a2f426cd79d3335ea017 v0.2 4 | 05f7d5da4e2f59b438ee02e8ef3931c13f08b0a8 v0.3-pre1 5 | 61cde7731947ef3a52fce5e6188cf2526c4af652 v0.3 6 | b70e5e25acfb857f9f855e5a50e6b2ccfa210810 v0.3.1 7 | 107852c4bc91eb7ec69d714f012018c816c169e1 v0.4-pre1 8 | 8b43f7f1b4104ae9e1fa69f9b01a9bb77a818844 v0.4-pre2 9 | ab1cecbc3720ac56d90e8b49cd823c77cc4c58e3 v0.4-pre3 10 | f33557df7510977738041630239902160c7c1f8c v0.4 11 | e88c915783935b9a5e6bcf9f28f01c93e244b3f7 v0.4.1 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Daniel K. O. 2 | Maschell 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM devkitpro/devkitppc 2 | # FROM ghcr.io/wiiu-env/devkitppc:20241128 3 | FROM dkosmari/devkitppc-wiiu-alpine 4 | 5 | COPY --from=ghcr.io/wiiu-env/libbuttoncombo:20250127 /artifacts $DEVKITPRO 6 | COPY --from=ghcr.io/wiiu-env/libmappedmemory:20250204 /artifacts $DEVKITPRO 7 | COPY --from=ghcr.io/wiiu-env/libnotifications:20250204 /artifacts $DEVKITPRO 8 | COPY --from=ghcr.io/wiiu-env/wiiupluginsystem:20250208 /artifacts $DEVKITPRO 9 | 10 | COPY --chown=user . . 11 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | @INC_AMINCLUDE@ 2 | 3 | 4 | EXTRA_DIST = \ 5 | assets \ 6 | AUTHORS \ 7 | bootstrap \ 8 | COPYING \ 9 | README.md 10 | 11 | 12 | SUBDIRS = external/libwupsxx 13 | 14 | 15 | AM_CPPFLAGS = -I$(srcdir)/external/libwupsxx/include 16 | 17 | AM_CXXFLAGS = -Wall -Wextra -Werror 18 | 19 | AM_LDFLAGS = $(WIIU_WUMS_LIBMAPPEDMEMORY_LDFLAGS) 20 | 21 | LDADD = \ 22 | external/libwupsxx/src/libwupsxx.a \ 23 | -lbuttoncombo \ 24 | -lmappedmemory \ 25 | -lnotifications 26 | 27 | 28 | WPS_FILE = papaya-hud.wps 29 | noinst_PROGRAMS = papaya-hud.elf 30 | 31 | papaya_hud_elf_SOURCES = \ 32 | src/ax_mon.cpp \ 33 | src/ax_mon.hpp \ 34 | src/cfg.cpp \ 35 | src/cfg.hpp \ 36 | src/cpu_mon.cpp \ 37 | src/cpu_mon.hpp \ 38 | src/fs_mon.cpp \ 39 | src/fs_mon.hpp \ 40 | src/gx2_mon.cpp \ 41 | src/gx2_mon.hpp \ 42 | src/gx2_perf.h \ 43 | src/main.cpp \ 44 | src/net_mon.cpp \ 45 | src/net_mon.hpp \ 46 | src/out_span.cpp \ 47 | src/out_span.hpp \ 48 | src/overlay.cpp \ 49 | src/overlay.hpp \ 50 | src/pad_mon.cpp \ 51 | src/pad_mon.hpp \ 52 | src/time_mon.cpp \ 53 | src/time_mon.hpp \ 54 | src/utils.cpp \ 55 | src/utils.hpp 56 | 57 | 58 | all-local: $(WPS_FILE) 59 | 60 | # $(WPS_FILE): papaya-hud.elf 61 | 62 | 63 | install-exec-local: $(WPS_FILE) 64 | curl "ftp://wiiu:/fs/vol/external01/wiiu/environments/aroma/plugins/" --upload-file $(WPS_FILE) 65 | 66 | 67 | uninstall-local: 68 | curl "ftp://wiiu" --quote "DELE /fs/vol/external01/wiiu/environments/aroma/plugins/$(WPS_FILE)" 69 | 70 | .PHONY: run company 71 | 72 | run: $(WPS_FILE) 73 | WIILOAD=tcp:wiiu wiiload $(WPS_FILE) 74 | 75 | 76 | .PHONY: company 77 | company: compile_flags.txt 78 | 79 | compile_flags.txt: Makefile 80 | printf "%s" "$(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS)" | xargs -n1 | sort -u > $(srcdir)/compile_flags.txt 81 | $(CPP) -xc++ /dev/null -E -Wp,-v 2>&1 | sed -n 's,^ ,-I,p' >> $(srcdir)/compile_flags.txt 82 | 83 | 84 | CLEANFILES = $(WPS_FILE) 85 | DISTCLEANFILES = $(AMINCLUDE) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Papaya HUD 2 | 3 | This is a HUD (Heads-Up Display) plugin for the Aroma environment on the Wii U. 4 | 5 |

6 | 7 | 8 | 9 |

10 | 11 | 12 | ## Features 13 | 14 | Supported stats: 15 | 16 | - Current time, console uptime, game play time. 17 | 18 | - Frames per second. 19 | 20 | - Screen resolution (TV / GamePad.) 21 | 22 | - CPU utilization, for all 3 PowerPC cores, and the ARM core (optional.) 23 | > **Note:** reading ARM core utilization might reduce the frame rate in games that 24 | > already have frame rate issues. 25 | 26 | - GPU utilization. 27 | > **Note:** this might reduce frame rate in some games. 28 | 29 | - Network profile (WiFi SSID, Ethernet, or none.) 30 | 31 | - Network bandwidth. 32 | 33 | - Filesystem performance. 34 | 35 | - Audio mode (mono, stereo, surround, and frequency.) 36 | 37 | - Audio utilization. 38 | > **Note:** this might lower frame rate in some games. 39 | 40 | - Button presses per second. 41 | 42 | - Controller battery level. 43 | 44 | You can also use a button shortcut to toggle the HUD on or off. By default it's **TV + L** 45 | on the gamepad, but you can change it in the plugin config menu (**L + DOWN + SELECT**). 46 | 47 | > The toggle state is **NOT** saved when using the toggle shortcut; this is done on purpose, 48 | > in case the game/app crashes when the HUD is turned on. The settings are only saved when 49 | > using the plugin config menu. 50 | 51 | The HUD color is also configurable. You can press **Y** while changing the color, to 52 | switch between RGB and hex mode. 53 | 54 | > Note that any option can be reset back to the default value by pressing **X** while 55 | > editing it. 56 | 57 | 58 | ## Building 59 | 60 | This plugin can be built in 3 different ways: locally, with Docker, or through Github Actions. 61 | 62 | 63 | ### Local build instructions 64 | 65 | This is an Automake package that's intended to be cross-compiled using devkitPro's 66 | environment. Besides the base Wii U packages (installable through `dkp-pacman`), you will 67 | also need to manually install the dependencies: 68 | 69 | - [wut](https://github.com/devkitPro/wut/) (because the latest devkitPro package is 70 | outdated.) 71 | 72 | - [libbuttoncombo](https://github.com/wiiu-env/libbuttoncombo) 73 | 74 | - [libmappedmemory](https://github.com/wiiu-env/libmappedmemory) 75 | 76 | - [libnotifications](https://github.com/wiiu-env/libnotifications) 77 | 78 | - [WiiUPluginSystem](https://github.com/wiiu-env/WiiUPluginSystem) 79 | 80 | 81 | To get the Papaya HUD source, either extract a release tarball (.tar.gz), or clone the 82 | code with git (remember to clone with the `--recurse-submodules` option.) 83 | 84 | Build steps (skip step 0 if you used a release tarball): 85 | 86 | 0. `./bootstrap` 87 | 88 | 1. `./configure --host=powerpc-eabi CXXFLAGS="-Os -ffunction-sections -fipa-pta"` 89 | 90 | 2. `make` 91 | 92 | 3. (Optional) If your Wii U is named `wiiu` in your local network, you can also do: 93 | 94 | - `make run` (will temporarily load the plugin into Aroma without installing it, 95 | requires `wiiload` package from devktiPro) 96 | 97 | - `make install` (requires `curl` from your system) 98 | 99 | - `make uninstall` (requires `curl` from your system) 100 | 101 | 102 | ### Docker build instructions 103 | 104 | If you have Docker, just run the `./docker-build.sh` script. 105 | 106 | 107 | ### Github Actions build instructions 108 | 109 | If you fork the repository, you can create builds using Github Actions: 110 | 111 | 1. Click on **Actions**. 112 | 113 | - If prompted, enable actions for your fork. 114 | 115 | 2. Click on **Build**, on the left. 116 | 117 | 3. Click on **Run workflow** on the right, then again on the **Run workflow** button. 118 | 119 | 4. Wait a few seconds and refresh the page. You will see the **Build** action being 120 | queued and executed. 121 | 122 | 5. After the build finishes (the status icon turns green), refresh the page. You can find 123 | the `.wps` file listed as an artifact, at the bottom. 124 | -------------------------------------------------------------------------------- /assets/hbasbadge-wiiu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkosmari/Papaya-HUD/44be9e76b1d7432132035649c8f3db8a1cb050ae/assets/hbasbadge-wiiu.png -------------------------------------------------------------------------------- /assets/papaya-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkosmari/Papaya-HUD/44be9e76b1d7432132035649c8f3db8a1cb050ae/assets/papaya-banner.png -------------------------------------------------------------------------------- /assets/papaya-banner.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkosmari/Papaya-HUD/44be9e76b1d7432132035649c8f3db8a1cb050ae/assets/papaya-banner.xcf -------------------------------------------------------------------------------- /assets/papaya-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkosmari/Papaya-HUD/44be9e76b1d7432132035649c8f3db8a1cb050ae/assets/papaya-icon.png -------------------------------------------------------------------------------- /assets/papaya-icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkosmari/Papaya-HUD/44be9e76b1d7432132035649c8f3db8a1cb050ae/assets/papaya-icon.xcf -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | (cd external/libwupsxx && ./bootstrap "$@") 3 | exec autoreconf --install "$@" 4 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | # -*- Autoconf -*- 2 | # Process this file with autoconf to produce a configure script. 3 | 4 | AC_PREREQ([2.69]) 5 | AC_INIT([Papaya HUD], [0.4.1], 6 | [https://github.com/dkosmari/Papaya-HUD/issues], 7 | [papaya-hud], 8 | [https://github.com/dkosmari/Papaya-HUD]) 9 | AC_CONFIG_SRCDIR([src/main.cpp]) 10 | AC_CONFIG_HEADERS([config.h]) 11 | AC_CONFIG_MACRO_DIR([m4]) 12 | AC_CONFIG_AUX_DIR([build-aux]) 13 | 14 | WIIU_WUPS_INIT 15 | WIIU_WUMS_INIT 16 | 17 | AM_INIT_AUTOMAKE([foreign subdir-objects]) 18 | 19 | AC_PROG_CXX 20 | AC_LANG([C++]) 21 | AX_APPEND_COMPILE_FLAGS([-std=c++23], [CXX]) 22 | 23 | WIIU_WUPS_SETUP 24 | WIIU_WUMS_SETUP 25 | WIIU_WUMS_CHECK_LIBBUTTONCOMBO 26 | WIIU_WUMS_CHECK_LIBMAPPEDMEMORY 27 | WIIU_WUMS_CHECK_LIBNOTIFICATIONS 28 | 29 | AX_SUBDIRS_CONFIGURE([external/libwupsxx], [--enable-shortcuts]) 30 | 31 | AC_CONFIG_FILES([Makefile]) 32 | AC_OUTPUT 33 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PLUGIN=papaya-hud 4 | IMAGE=${PLUGIN}-image 5 | CONTAINER=${PLUGIN}-container 6 | 7 | cleanup() 8 | { 9 | echo "Cleaning up Docker..." 10 | if test x$CONTAINER != x 11 | then 12 | docker container rm --force $CONTAINER 13 | fi 14 | if test x$IMAGE != x 15 | then 16 | docker image rm --force $IMAGE 17 | fi 18 | echo "You may also want to run 'docker system prune --force' to delete Docker's caches." 19 | exit $1 20 | } 21 | trap cleanup INT TERM 22 | 23 | docker build --tag $IMAGE . || cleanup 1 24 | 25 | ARGS="--tty --interactive --name $CONTAINER $IMAGE" 26 | docker run $ARGS sh -c "./bootstrap && ./configure --host=powerpc-eabi CXXFLAGS='-Os -ffunction-sections -fipa-pta -Wno-odr -flto' AR=powerpc-eabi-gcc-ar RANLIB=powerpc-eabi-gcc-ranlib && make" || cleanup 2 27 | echo "Compilation finished." 28 | 29 | # Copy the wps file out. 30 | docker cp "$CONTAINER:/home/user/src/${PLUGIN}.wps" . 31 | 32 | cleanup 0 33 | -------------------------------------------------------------------------------- /m4/ax_add_am_macro.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_add_am_macro.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_ADD_AM_MACRO([RULE]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # Adds the specified rule to $AMINCLUDE. This macro will only work 12 | # properly with implementations of Make which allow include statements. 13 | # See also AX_ADD_AM_MACRO_STATIC. 14 | # 15 | # LICENSE 16 | # 17 | # Copyright (c) 2009 Tom Howard 18 | # 19 | # Copying and distribution of this file, with or without modification, are 20 | # permitted in any medium without royalty provided the copyright notice 21 | # and this notice are preserved. This file is offered as-is, without any 22 | # warranty. 23 | 24 | #serial 10 25 | 26 | AC_DEFUN([AX_ADD_AM_MACRO],[ 27 | AC_REQUIRE([AX_AM_MACROS]) 28 | AX_APPEND_TO_FILE([$AMINCLUDE],[$1]) 29 | ]) 30 | -------------------------------------------------------------------------------- /m4/ax_am_macros.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_am_macros.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_AM_MACROS 8 | # 9 | # DESCRIPTION 10 | # 11 | # Adds support for macros that create Make rules. You must manually add 12 | # the following line 13 | # 14 | # @INC_AMINCLUDE@ 15 | # 16 | # to your Makefile.in (or Makefile.am if you use Automake) files. 17 | # 18 | # LICENSE 19 | # 20 | # Copyright (c) 2009 Tom Howard 21 | # 22 | # Copying and distribution of this file, with or without modification, are 23 | # permitted in any medium without royalty provided the copyright notice 24 | # and this notice are preserved. This file is offered as-is, without any 25 | # warranty. 26 | 27 | #serial 11 28 | 29 | AC_DEFUN([AX_AM_MACROS], 30 | [ 31 | AC_MSG_NOTICE([adding automake macro support]) 32 | AMINCLUDE="aminclude.am" 33 | AC_SUBST(AMINCLUDE) 34 | AC_MSG_NOTICE([creating $AMINCLUDE]) 35 | AMINCLUDE_TIME=`LC_ALL=C date` 36 | AX_PRINT_TO_FILE([$AMINCLUDE],[[ 37 | # generated automatically by configure from AX_AUTOMAKE_MACROS 38 | # on $AMINCLUDE_TIME 39 | 40 | ]]) 41 | 42 | INC_AMINCLUDE="include \$(top_builddir)/$AMINCLUDE" 43 | AC_SUBST(INC_AMINCLUDE) 44 | ]) 45 | -------------------------------------------------------------------------------- /m4/ax_append_compile_flags.m4: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # https://www.gnu.org/software/autoconf-archive/ax_append_compile_flags.html 3 | # ============================================================================ 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_APPEND_COMPILE_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # For every FLAG1, FLAG2 it is checked whether the compiler works with the 12 | # flag. If it does, the flag is added FLAGS-VARIABLE 13 | # 14 | # If FLAGS-VARIABLE is not specified, the current language's flags (e.g. 15 | # CFLAGS) is used. During the check the flag is always added to the 16 | # current language's flags. 17 | # 18 | # If EXTRA-FLAGS is defined, it is added to the current language's default 19 | # flags (e.g. CFLAGS) when the check is done. The check is thus made with 20 | # the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to 21 | # force the compiler to issue an error when a bad flag is given. 22 | # 23 | # INPUT gives an alternative input source to AC_COMPILE_IFELSE. 24 | # 25 | # NOTE: This macro depends on the AX_APPEND_FLAG and 26 | # AX_CHECK_COMPILE_FLAG. Please keep this macro in sync with 27 | # AX_APPEND_LINK_FLAGS. 28 | # 29 | # LICENSE 30 | # 31 | # Copyright (c) 2011 Maarten Bosmans 32 | # 33 | # Copying and distribution of this file, with or without modification, are 34 | # permitted in any medium without royalty provided the copyright notice 35 | # and this notice are preserved. This file is offered as-is, without any 36 | # warranty. 37 | 38 | #serial 7 39 | 40 | AC_DEFUN([AX_APPEND_COMPILE_FLAGS], 41 | [AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) 42 | AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) 43 | for flag in $1; do 44 | AX_CHECK_COMPILE_FLAG([$flag], [AX_APPEND_FLAG([$flag], [$2])], [], [$3], [$4]) 45 | done 46 | ])dnl AX_APPEND_COMPILE_FLAGS 47 | -------------------------------------------------------------------------------- /m4/ax_append_flag.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_append_flag.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_APPEND_FLAG(FLAG, [FLAGS-VARIABLE]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # FLAG is appended to the FLAGS-VARIABLE shell variable, with a space 12 | # added in between. 13 | # 14 | # If FLAGS-VARIABLE is not specified, the current language's flags (e.g. 15 | # CFLAGS) is used. FLAGS-VARIABLE is not changed if it already contains 16 | # FLAG. If FLAGS-VARIABLE is unset in the shell, it is set to exactly 17 | # FLAG. 18 | # 19 | # NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. 20 | # 21 | # LICENSE 22 | # 23 | # Copyright (c) 2008 Guido U. Draheim 24 | # Copyright (c) 2011 Maarten Bosmans 25 | # 26 | # Copying and distribution of this file, with or without modification, are 27 | # permitted in any medium without royalty provided the copyright notice 28 | # and this notice are preserved. This file is offered as-is, without any 29 | # warranty. 30 | 31 | #serial 8 32 | 33 | AC_DEFUN([AX_APPEND_FLAG], 34 | [dnl 35 | AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_SET_IF 36 | AS_VAR_PUSHDEF([FLAGS], [m4_default($2,_AC_LANG_PREFIX[FLAGS])]) 37 | AS_VAR_SET_IF(FLAGS,[ 38 | AS_CASE([" AS_VAR_GET(FLAGS) "], 39 | [*" $1 "*], [AC_RUN_LOG([: FLAGS already contains $1])], 40 | [ 41 | AS_VAR_APPEND(FLAGS,[" $1"]) 42 | AC_RUN_LOG([: FLAGS="$FLAGS"]) 43 | ]) 44 | ], 45 | [ 46 | AS_VAR_SET(FLAGS,[$1]) 47 | AC_RUN_LOG([: FLAGS="$FLAGS"]) 48 | ]) 49 | AS_VAR_POPDEF([FLAGS])dnl 50 | ])dnl AX_APPEND_FLAG 51 | -------------------------------------------------------------------------------- /m4/ax_append_to_file.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_append_to_file.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_APPEND_TO_FILE([FILE],[DATA]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # Appends the specified data to the specified file. 12 | # 13 | # LICENSE 14 | # 15 | # Copyright (c) 2008 Tom Howard 16 | # 17 | # Copying and distribution of this file, with or without modification, are 18 | # permitted in any medium without royalty provided the copyright notice 19 | # and this notice are preserved. This file is offered as-is, without any 20 | # warranty. 21 | 22 | #serial 9 23 | 24 | AC_DEFUN([AX_APPEND_TO_FILE],[ 25 | AC_REQUIRE([AX_FILE_ESCAPES]) 26 | printf "%s" "$2" >> "$1" 27 | ]) 28 | -------------------------------------------------------------------------------- /m4/ax_check_compile_flag.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_check_compile_flag.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_CHECK_COMPILE_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # Check whether the given FLAG works with the current language's compiler 12 | # or gives an error. (Warnings, however, are ignored) 13 | # 14 | # ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on 15 | # success/failure. 16 | # 17 | # If EXTRA-FLAGS is defined, it is added to the current language's default 18 | # flags (e.g. CFLAGS) when the check is done. The check is thus made with 19 | # the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to 20 | # force the compiler to issue an error when a bad flag is given. 21 | # 22 | # INPUT gives an alternative input source to AC_COMPILE_IFELSE. 23 | # 24 | # NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this 25 | # macro in sync with AX_CHECK_{PREPROC,LINK}_FLAG. 26 | # 27 | # LICENSE 28 | # 29 | # Copyright (c) 2008 Guido U. Draheim 30 | # Copyright (c) 2011 Maarten Bosmans 31 | # 32 | # Copying and distribution of this file, with or without modification, are 33 | # permitted in any medium without royalty provided the copyright notice 34 | # and this notice are preserved. This file is offered as-is, without any 35 | # warranty. 36 | 37 | #serial 6 38 | 39 | AC_DEFUN([AX_CHECK_COMPILE_FLAG], 40 | [AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF 41 | AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl 42 | AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [ 43 | ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS 44 | _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1" 45 | AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])], 46 | [AS_VAR_SET(CACHEVAR,[yes])], 47 | [AS_VAR_SET(CACHEVAR,[no])]) 48 | _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags]) 49 | AS_VAR_IF(CACHEVAR,yes, 50 | [m4_default([$2], :)], 51 | [m4_default([$3], :)]) 52 | AS_VAR_POPDEF([CACHEVAR])dnl 53 | ])dnl AX_CHECK_COMPILE_FLAGS 54 | -------------------------------------------------------------------------------- /m4/ax_check_library.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_check_library.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_CHECK_LIBRARY(VARIABLE-PREFIX, HEADER-FILE, LIBRARY-FILE, 8 | # [ACTION-IF-FOUND], [ACTION-IF-NOT_FOUND]) 9 | # 10 | # DESCRIPTION 11 | # 12 | # Provides a generic test for a given library, similar in concept to the 13 | # PKG_CHECK_MODULES macro used by pkg-config. 14 | # 15 | # Most simplest libraries can be checked against simply through the 16 | # presence of a header file and a library to link to. This macro allows to 17 | # wrap around the test so that it doesn't have to be recreated each time. 18 | # 19 | # Rather than define --with-$LIBRARY arguments, it uses variables in the 20 | # same way that PKG_CHECK_MODULES does. It doesn't, though, use the same 21 | # names, since you shouldn't provide a value for LIBS or CFLAGS but rather 22 | # for LDFLAGS and CPPFLAGS, to tell the linker and compiler where to find 23 | # libraries and headers respectively. 24 | # 25 | # If the library is find, HAVE_PREFIX is defined, and in all cases 26 | # PREFIX_LDFLAGS and PREFIX_CPPFLAGS are substituted. 27 | # 28 | # Example: 29 | # 30 | # AX_CHECK_LIBRARY([LIBEVENT], [event.h], [event], [], 31 | # [AC_MSG_ERROR([Unable to find libevent])]) 32 | # 33 | # LICENSE 34 | # 35 | # Copyright (c) 2010 Diego Elio Petteno` 36 | # 37 | # This program is free software: you can redistribute it and/or modify it 38 | # under the terms of the GNU General Public License as published by the 39 | # Free Software Foundation, either version 3 of the License, or (at your 40 | # option) any later version. 41 | # 42 | # This program is distributed in the hope that it will be useful, but 43 | # WITHOUT ANY WARRANTY; without even the implied warranty of 44 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 45 | # Public License for more details. 46 | # 47 | # You should have received a copy of the GNU General Public License along 48 | # with this program. If not, see . 49 | # 50 | # As a special exception, the respective Autoconf Macro's copyright owner 51 | # gives unlimited permission to copy, distribute and modify the configure 52 | # scripts that are the output of Autoconf when processing the Macro. You 53 | # need not follow the terms of the GNU General Public License when using 54 | # or distributing such scripts, even though portions of the text of the 55 | # Macro appear in them. The GNU General Public License (GPL) does govern 56 | # all other use of the material that constitutes the Autoconf Macro. 57 | # 58 | # This special exception to the GPL applies to versions of the Autoconf 59 | # Macro released by the Autoconf Archive. When you make and distribute a 60 | # modified version of the Autoconf Macro, you may extend this special 61 | # exception to the GPL to apply to your modified version as well. 62 | 63 | #serial 5 64 | 65 | AC_DEFUN([AX_CHECK_LIBRARY], [ 66 | AC_ARG_VAR($1[_CPPFLAGS], [C preprocessor flags for ]$1[ headers]) 67 | AC_ARG_VAR($1[_LDFLAGS], [linker flags for ]$1[ libraries]) 68 | 69 | AC_CACHE_VAL(AS_TR_SH([ax_cv_have_]$1), 70 | [save_CPPFLAGS="$CPPFLAGS" 71 | save_LDFLAGS="$LDFLAGS" 72 | save_LIBS="$LIBS" 73 | 74 | AS_IF([test "x$]$1[_CPPFLAGS" != "x"], 75 | [CPPFLAGS="$CPPFLAGS $]$1[_CPPFLAGS"]) 76 | 77 | AS_IF([test "x$]$1[_LDFLAGS" != "x"], 78 | [LDFLAGS="$LDFLAGS $]$1[_LDFLAGS"]) 79 | 80 | AC_CHECK_HEADER($2, [ 81 | AC_CHECK_LIB($3, [main], 82 | [AS_TR_SH([ax_cv_have_]$1)=yes], 83 | [AS_TR_SH([ax_cv_have_]$1)=no]) 84 | ], [AS_TR_SH([ax_cv_have_]$1)=no]) 85 | 86 | CPPFLAGS="$save_CPPFLAGS" 87 | LDFLAGS="$save_LDFLAGS" 88 | LIBS="$save_LIBS" 89 | ]) 90 | 91 | AS_IF([test "$]AS_TR_SH([ax_cv_have_]$1)[" = "yes"], 92 | AC_DEFINE([HAVE_]$1, [1], [Define to 1 if ]$1[ is found]) 93 | [$4], 94 | [$5]) 95 | ]) 96 | -------------------------------------------------------------------------------- /m4/ax_file_escapes.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_file_escapes.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_FILE_ESCAPES 8 | # 9 | # DESCRIPTION 10 | # 11 | # Writes the specified data to the specified file. 12 | # 13 | # LICENSE 14 | # 15 | # Copyright (c) 2008 Tom Howard 16 | # 17 | # Copying and distribution of this file, with or without modification, are 18 | # permitted in any medium without royalty provided the copyright notice 19 | # and this notice are preserved. This file is offered as-is, without any 20 | # warranty. 21 | 22 | #serial 8 23 | 24 | AC_DEFUN([AX_FILE_ESCAPES],[ 25 | AX_DOLLAR="\$" 26 | AX_SRB="\\135" 27 | AX_SLB="\\133" 28 | AX_BS="\\\\" 29 | AX_DQ="\"" 30 | ]) 31 | -------------------------------------------------------------------------------- /m4/ax_prepend_flag.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_prepend_flag.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_PREPEND_FLAG(FLAG, [FLAGS-VARIABLE]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # FLAG is added to the front of the FLAGS-VARIABLE shell variable, with a 12 | # space added in between. 13 | # 14 | # If FLAGS-VARIABLE is not specified, the current language's flags (e.g. 15 | # CFLAGS) is used. FLAGS-VARIABLE is not changed if it already contains 16 | # FLAG. If FLAGS-VARIABLE is unset in the shell, it is set to exactly 17 | # FLAG. 18 | # 19 | # NOTE: Implementation based on AX_APPEND_FLAG. 20 | # 21 | # LICENSE 22 | # 23 | # Copyright (c) 2008 Guido U. Draheim 24 | # Copyright (c) 2011 Maarten Bosmans 25 | # Copyright (c) 2018 John Zaitseff 26 | # 27 | # Copying and distribution of this file, with or without modification, are 28 | # permitted in any medium without royalty provided the copyright notice 29 | # and this notice are preserved. This file is offered as-is, without any 30 | # warranty. 31 | 32 | #serial 2 33 | 34 | AC_DEFUN([AX_PREPEND_FLAG], 35 | [dnl 36 | AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_SET_IF 37 | AS_VAR_PUSHDEF([FLAGS], [m4_default($2,_AC_LANG_PREFIX[FLAGS])]) 38 | AS_VAR_SET_IF(FLAGS,[ 39 | AS_CASE([" AS_VAR_GET(FLAGS) "], 40 | [*" $1 "*], [AC_RUN_LOG([: FLAGS already contains $1])], 41 | [ 42 | FLAGS="$1 $FLAGS" 43 | AC_RUN_LOG([: FLAGS="$FLAGS"]) 44 | ]) 45 | ], 46 | [ 47 | AS_VAR_SET(FLAGS,[$1]) 48 | AC_RUN_LOG([: FLAGS="$FLAGS"]) 49 | ]) 50 | AS_VAR_POPDEF([FLAGS])dnl 51 | ])dnl AX_PREPEND_FLAG 52 | -------------------------------------------------------------------------------- /m4/ax_print_to_file.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_print_to_file.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_PRINT_TO_FILE([FILE],[DATA]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # Writes the specified data to the specified file. 12 | # 13 | # LICENSE 14 | # 15 | # Copyright (c) 2008 Tom Howard 16 | # 17 | # Copying and distribution of this file, with or without modification, are 18 | # permitted in any medium without royalty provided the copyright notice 19 | # and this notice are preserved. This file is offered as-is, without any 20 | # warranty. 21 | 22 | #serial 8 23 | 24 | AC_DEFUN([AX_PRINT_TO_FILE],[ 25 | AC_REQUIRE([AX_FILE_ESCAPES]) 26 | printf "$2" > "$1" 27 | ]) 28 | -------------------------------------------------------------------------------- /m4/ax_require_defined.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_require_defined.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_REQUIRE_DEFINED(MACRO) 8 | # 9 | # DESCRIPTION 10 | # 11 | # AX_REQUIRE_DEFINED is a simple helper for making sure other macros have 12 | # been defined and thus are available for use. This avoids random issues 13 | # where a macro isn't expanded. Instead the configure script emits a 14 | # non-fatal: 15 | # 16 | # ./configure: line 1673: AX_CFLAGS_WARN_ALL: command not found 17 | # 18 | # It's like AC_REQUIRE except it doesn't expand the required macro. 19 | # 20 | # Here's an example: 21 | # 22 | # AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG]) 23 | # 24 | # LICENSE 25 | # 26 | # Copyright (c) 2014 Mike Frysinger 27 | # 28 | # Copying and distribution of this file, with or without modification, are 29 | # permitted in any medium without royalty provided the copyright notice 30 | # and this notice are preserved. This file is offered as-is, without any 31 | # warranty. 32 | 33 | #serial 2 34 | 35 | AC_DEFUN([AX_REQUIRE_DEFINED], [dnl 36 | m4_ifndef([$1], [m4_fatal([macro ]$1[ is not defined; is a m4 file missing?])]) 37 | ])dnl AX_REQUIRE_DEFINED 38 | -------------------------------------------------------------------------------- /m4/ax_subdirs_configure.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_subdirs_configure.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_SUBDIRS_CONFIGURE( [subdirs], [mandatory arguments], [possibly merged arguments], [replacement arguments], [forbidden arguments]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # AX_SUBDIRS_CONFIGURE attempts to be the equivalent of AC_CONFIG_SUBDIRS 12 | # with customizable options for configure scripts. 13 | # 14 | # Run the configure script for each directory from the comma-separated m4 15 | # list 'subdirs'. This macro can be used multiple times. All arguments of 16 | # this macro must be comma-separated lists. 17 | # 18 | # All command line arguments from the parent configure script will be 19 | # given to the subdirectory configure script after the following 20 | # modifications (in that order): 21 | # 22 | # 1. The arguments from the 'mandatory arguments' list shall always be 23 | # appended to the argument list. 24 | # 25 | # 2. The arguments from the 'possibly merged arguments' list shall be 26 | # added if not present in the arguments of the parent configure script or 27 | # merged with the existing argument otherwise. 28 | # 29 | # 3. The arguments from the 'replacement arguments' list shall be added if 30 | # not present in the arguments of the parent configure script or replace 31 | # the existing argument otherwise. 32 | # 33 | # 4. The arguments from the 'forbidden arguments' list shall always be 34 | # removed from the argument list. 35 | # 36 | # The lists 'mandatory arguments' and 'forbidden arguments' can hold any 37 | # kind of argument. The 'possibly merged arguments' and 'replacement 38 | # arguments' expect their arguments to be of the form --option-name=value. 39 | # 40 | # This macro aims to remain as close as possible to the AC_CONFIG_SUBDIRS 41 | # macro. It corrects the paths for '--srcdir' and adds 42 | # '--disable-option-checking' and '--silent' if necessary. However, it 43 | # does not change the '--cache-file' argument: typically, configure 44 | # scripts run with different arguments will not be able to share the same 45 | # cache. If you wish to share a single cache, you should give an absolute 46 | # path to '--cache-file'. 47 | # 48 | # This macro also sets the output variable subdirs_extra to the list of 49 | # directories recorded with AX_SUBDIRS_CONFIGURE. This variable can be 50 | # used in Makefile rules or substituted in configured files. 51 | # 52 | # This macro shall do nothing more than managing the arguments of the 53 | # configure script. Just like when using AC_CONFIG_SUBDIRS, it is up to 54 | # the user to check any requirements or define and substitute any required 55 | # variable for the remainder of the project. 56 | # 57 | # Configure scripts recorded with AX_SUBDIRS_CONFIGURE may be executed 58 | # before configure scripts recorded with AC_CONFIG_SUBDIRS. 59 | # 60 | # Without additional arguments, the behaviour of AX_SUBDIRS_CONFIGURE 61 | # should be identical to the behaviour of AC_CONFIG_SUBDIRS, apart from 62 | # the contents of the variables subdirs and subdirs_extra (except that 63 | # AX_SUBDIRS_CONFIGURE expects a comma-separated m4 list): 64 | # 65 | # AC_CONFIG_SUBDIRS([something]) 66 | # AX_SUBDIRS_CONFIGURE([something]) 67 | # 68 | # This macro may be called multiple times. 69 | # 70 | # Usage example: 71 | # 72 | # Let us assume our project has 4 dependencies, namely A, B, C and D. Here 73 | # are some characteristics of our project and its dependencies: 74 | # 75 | # - A does not require any special option. 76 | # 77 | # - we want to build B with an optional feature which can be enabled with 78 | # its configure script's option '--enable-special-feature'. 79 | # 80 | # - B's configure script is strange and has an option '--with-B=build'. 81 | # After close inspection of its documentation, we don't want B to receive 82 | # this option. 83 | # 84 | # - C and D both need B. 85 | # 86 | # - Just like our project, C and D can build B themselves with the option 87 | # '--with-B=build'. 88 | # 89 | # - We want C and D to use the B we build instead of building it 90 | # themselves. 91 | # 92 | # Our top-level configure script will be called as follows: 93 | # 94 | # $ --with-A=build --with-B=build --with-C=build \ 95 | # --with-D=build --some-option 96 | # 97 | # Thus we have to make sure that: 98 | # 99 | # - neither B, C or D receive the option '--with-B=build' 100 | # 101 | # - C and D know where to find the headers and libraries of B. 102 | # 103 | # Under those conditions, we can use the AC_CONFIG_SUBDIRS macro for A, 104 | # but need to use AX_SUBDIRS_CONFIGURE for B, C and D: 105 | # 106 | # - B must receive '--enable-special-feature' but cannot receive 107 | # '--with-B=build' 108 | # 109 | # - C and D cannot receive '--with-B=build' (or else it would be built 110 | # thrice) and need to be told where to find B (since we are building it, 111 | # it would probably not be available in standard paths). 112 | # 113 | # Here is a configure.ac snippet that solves our problem: 114 | # 115 | # AC_CONFIG_SUBDIRS([dependencies/A]) 116 | # AX_SUBDIRS_CONFIGURE( 117 | # [dependencies/B], [--enable-special-feature], [], [], 118 | # [--with-B=build]) 119 | # AX_SUBDIRS_CONFIGURE( 120 | # [[dependencies/C],[dependencies/D]], 121 | # [], 122 | # [[CPPFLAGS=-I${ac_top_srcdir}/dependencies/B -I${ac_top_builddir}/dependencies/B], 123 | # [LDFLAGS=-L${ac_abs_top_builddir}/dependencies/B/.libs]], 124 | # [--with-B=system], 125 | # []) 126 | # 127 | # If using automake, the following can be added to the Makefile.am (we use 128 | # both $(subdirs) and $(subdirs_extra) since our example above used both 129 | # AC_CONFIG_SUBDIRS and AX_SUBDIRS_CONFIGURE): 130 | # 131 | # SUBDIRS = $(subdirs) $(subdirs_extra) 132 | # 133 | # LICENSE 134 | # 135 | # Copyright (c) 2017 Harenome Ranaivoarivony-Razanajato 136 | # 137 | # This program is free software; you can redistribute it and/or modify it 138 | # under the terms of the GNU General Public License as published by the 139 | # Free Software Foundation; either version 3 of the License, or (at your 140 | # option) any later version. 141 | # 142 | # This program is distributed in the hope that it will be useful, but 143 | # WITHOUT ANY WARRANTY; without even the implied warranty of 144 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 145 | # Public License for more details. 146 | # 147 | # Under Section 7 of GPL version 3, you are granted additional permissions 148 | # described in the Autoconf Configure Script Exception, version 3.0, as 149 | # published by the Free Software Foundation. 150 | # 151 | # You should have received a copy of the GNU General Public License along 152 | # with this program. If not, see . 153 | 154 | #serial 6 155 | 156 | AC_DEFUN([AX_SUBDIRS_CONFIGURE], 157 | [ 158 | dnl Calls to AC_CONFIG_SUBDIRS perform preliminary steps and build a list 159 | dnl '$subdirs' which is used later by _AC_OUTPUT_SUBDIRS (used by AC_OUTPUT) 160 | dnl to actually run the configure scripts. 161 | dnl This macro performs similar preliminary steps but uses 162 | dnl AC_CONFIG_COMMANDS_PRE to delay the final tasks instead of building an 163 | dnl intermediary list and relying on another macro. 164 | dnl 165 | dnl Since each configure script can get different options, a special variable 166 | dnl named 'ax_sub_configure_args_' is constructed for each 167 | dnl subdirectory. 168 | 169 | # Various preliminary checks. 170 | AC_REQUIRE([AC_DISABLE_OPTION_CHECKING]) 171 | AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT]) 172 | AS_LITERAL_IF([$1], [], 173 | [AC_DIAGNOSE([syntax], [$0: you should use literals])]) 174 | 175 | m4_foreach(subdir_path, [$1], 176 | [ 177 | ax_dir="subdir_path" 178 | 179 | dnl Build the argument list in a similar fashion to AC_CONFIG_SUBDIRS. 180 | dnl A few arguments found in the final call to the configure script are not 181 | dnl added here because they rely on variables that may not yet be available 182 | dnl (see below the part that is similar to _AC_OUTPUT_SUBDIRS). 183 | # Do not complain, so a configure script can configure whichever parts of a 184 | # large source tree are present. 185 | if test -d "$srcdir/$ax_dir"; then 186 | _AC_SRCDIRS(["$ax_dir"]) 187 | # Remove --cache-file, --srcdir, and --disable-option-checking arguments 188 | # so they do not pile up. 189 | ax_args= 190 | ax_prev= 191 | eval "set x $ac_configure_args" 192 | shift 193 | for ax_arg; do 194 | if test -n "$ax_prev"; then 195 | ax_prev= 196 | continue 197 | fi 198 | case $ax_arg in 199 | -cache-file | --cache-file | --cache-fil | --cache-fi | --cache-f \ 200 | | --cache- | --cache | --cach | --cac | --ca | --c) 201 | ax_prev=cache_file ;; 202 | -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \ 203 | | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* \ 204 | | --c=*) 205 | ;; 206 | --config-cache | -C) 207 | ;; 208 | -srcdir | --srcdir | --srcdi | --srcd | --src | --sr) 209 | ax_prev=srcdir ;; 210 | -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*) 211 | ;; 212 | -prefix | --prefix | --prefi | --pref | --pre | --pr | --p) 213 | ax_prev=prefix ;; 214 | -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* \ 215 | | --p=*) 216 | ;; 217 | --disable-option-checking) 218 | ;; 219 | *) case $ax_arg in 220 | *\'*) ax_arg=$(AS_ECHO(["$ax_arg"]) | sed "s/'/'\\\\\\\\''/g");; 221 | esac 222 | AS_VAR_APPEND([ax_args], [" '$ax_arg'"]) ;; 223 | esac 224 | done 225 | # Always prepend --disable-option-checking to silence warnings, since 226 | # different subdirs can have different --enable and --with options. 227 | ax_args="--disable-option-checking $ax_args" 228 | # Options that must be added as they are provided. 229 | m4_ifnblank([$2], [m4_foreach(opt, [$2], [AS_VAR_APPEND(ax_args, " 'opt'") 230 | ])]) 231 | # New options that may need to be merged with existing options. 232 | m4_ifnblank([$3], [m4_foreach(opt, [$3], 233 | [ax_candidate="opt" 234 | ax_candidate_flag="${ax_candidate%%=*}" 235 | ax_candidate_content="${ax_candidate#*=}" 236 | if test "x$ax_candidate" != "x" -a "x$ax_candidate_flag" != "x"; then 237 | if echo "$ax_args" | grep -- "${ax_candidate_flag}=" >/dev/null 2>&1; then 238 | [ax_args=$(echo $ax_args | sed "s,\(${ax_candidate_flag}=[^']*\),\1 ${ax_candidate_content},")] 239 | else 240 | AS_VAR_APPEND(ax_args, " 'opt'") 241 | fi 242 | fi 243 | ])]) 244 | # New options that must replace existing options. 245 | m4_ifnblank([$4], [m4_foreach(opt, [$4], 246 | [ax_candidate="opt" 247 | ax_candidate_flag="${ax_candidate%%=*}" 248 | ax_candidate_content="${ax_candidate#*=}" 249 | if test "x$ax_candidate" != "x" -a "x$ax_candidate_flag" != "x"; then 250 | if echo "$ax_args" | grep -- "${ax_candidate_flag}=" >/dev/null 2>&1; then 251 | [ax_args=$(echo $ax_args | sed "s,${ax_candidate_flag}=[^']*,${ax_candidate},")] 252 | else 253 | AS_VAR_APPEND(ax_args, " 'opt'") 254 | fi 255 | fi 256 | ])]) 257 | # Options that must be removed. 258 | m4_ifnblank([$5], [m4_foreach(opt, [$5], [ax_args=$(echo $ax_args | sed "s,'opt',,") 259 | ])]) 260 | AS_VAR_APPEND([ax_args], [" '--srcdir=$ac_srcdir'"]) 261 | 262 | # Add the subdirectory to the list of target subdirectories. 263 | ax_subconfigures="$ax_subconfigures $ax_dir" 264 | # Save the argument list for this subdirectory. 265 | dnl $1 is a path to some subdirectory: m4_bpatsubsts() is used to convert 266 | dnl $1 into a valid shell variable name. 267 | dnl For instance, "ax_sub_configure_args_path/to/subdir" becomes 268 | dnl "ax_sub_configure_args_path_to_subdir". 269 | ax_var=$(printf "$ax_dir" | tr -c "0-9a-zA-Z_" "_") 270 | eval "ax_sub_configure_args_$ax_var=\"$ax_args\"" 271 | eval "ax_sub_configure_$ax_var=\"yes\"" 272 | else 273 | AC_MSG_WARN([could not find source tree for $ax_dir]) 274 | fi 275 | 276 | dnl Add some more arguments to the argument list and then actually run the 277 | dnl configure script. This is mostly what happens in _AC_OUTPUT_SUBDIRS 278 | dnl except it does not iterate over an intermediary list. 279 | AC_CONFIG_COMMANDS_PRE( 280 | dnl This very line cannot be quoted! m4_foreach has some work here. 281 | ax_dir="subdir_path" 282 | [ 283 | # Convert the path to the subdirectory into a shell variable name. 284 | ax_var=$(printf "$ax_dir" | tr -c "0-9a-zA-Z_" "_") 285 | ax_configure_ax_var=$(eval "echo \"\$ax_sub_configure_$ax_var\"") 286 | if test "$no_recursion" != "yes" -a "x$ax_configure_ax_var" = "xyes"; then 287 | AC_SUBST([subdirs_extra], ["$subdirs_extra $ax_dir"]) 288 | ax_msg="=== configuring in $ax_dir ($(pwd)/$ax_dir)" 289 | _AS_ECHO_LOG([$ax_msg]) 290 | _AS_ECHO([$ax_msg]) 291 | AS_MKDIR_P(["$ax_dir"]) 292 | _AC_SRCDIRS(["$ax_dir"]) 293 | 294 | ax_popdir=$(pwd) 295 | cd "$ax_dir" 296 | 297 | # Check for guested configure; otherwise get Cygnus style configure. 298 | if test -f "$ac_srcdir/configure.gnu"; then 299 | ax_sub_configure=$ac_srcdir/configure.gnu 300 | elif test -f "$ac_srcdir/configure"; then 301 | ax_sub_configure=$ac_srcdir/configure 302 | elif test -f "$ac_srcdir/configure.in"; then 303 | # This should be Cygnus configure. 304 | ax_sub_configure=$ac_aux_dir/configure 305 | else 306 | AC_MSG_WARN([no configuration information is in $ax_dir]) 307 | ax_sub_configure= 308 | fi 309 | 310 | if test -n "$ax_sub_configure"; then 311 | # Get the configure arguments for the current configure. 312 | eval "ax_sub_configure_args=\"\$ax_sub_configure_args_${ax_var}\"" 313 | 314 | # Always prepend --prefix to ensure using the same prefix 315 | # in subdir configurations. 316 | ax_arg="--prefix=$prefix" 317 | case $ax_arg in 318 | *\'*) ax_arg=$(AS_ECHO(["$ax_arg"]) | sed "s/'/'\\\\\\\\''/g");; 319 | esac 320 | ax_sub_configure_args="'$ax_arg' $ax_sub_configure_args" 321 | if test "$silent" = yes; then 322 | ax_sub_configure_args="--silent $ax_sub_configure_args" 323 | fi 324 | 325 | AC_MSG_NOTICE([running $SHELL $ax_sub_configure $ax_sub_configure_args --cache-file=$cache_file]) 326 | eval "\$SHELL \"$ax_sub_configure\" $ax_sub_configure_args --cache-file=\"$cache_file\"" \ 327 | || AC_MSG_ERROR([$ax_sub_configure failed for $ax_dir]) 328 | fi 329 | 330 | cd "$ax_popdir" 331 | fi 332 | ]) 333 | ]) 334 | ]) 335 | -------------------------------------------------------------------------------- /m4/ax_var_push.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # https://www.gnu.org/software/autoconf-archive/ax_var_push.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_VAR_PUSHVALUE(VARIABLE, [VALUE]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # Stores a copy of variable_name's value and assigns it to 'value' If no 12 | # value is given, its original value is kept. Compile, link and running 13 | # tests usually require the programmer to provide additional flags. 14 | # However, it is strongly recommended not to override flags defined by the 15 | # user through the configure command. AX_VAR_PUSHVALUE and AX_VAR_POPVALUE 16 | # are clean way to temporarily store a variable's value and restore it 17 | # later, using a stack-like behaviour. This macro supports nesting. 18 | # 19 | # Example: 20 | # 21 | # AX_VAR_PUSHVALUE([CXXFLAGS],["my test flags"]) 22 | # perform some checks with CXXFLAGS... 23 | # CXXFLAGS value will be "my test flags" 24 | # AX_VAR_POPVALUE([CXXFLAGS]) 25 | # CXXFLAGS is restored to its original value 26 | # 27 | # LICENSE 28 | # 29 | # Copyright (c) 2015 Jorge Bellon 30 | # 31 | # This program is free software: you can redistribute it and/or modify it 32 | # under the terms of the GNU General Public License as published by the 33 | # Free Software Foundation, either version 3 of the License, or (at your 34 | # option) any later version. 35 | # 36 | # This program is distributed in the hope that it will be useful, but 37 | # WITHOUT ANY WARRANTY; without even the implied warranty of 38 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 39 | # Public License for more details. 40 | # 41 | # You should have received a copy of the GNU General Public License along 42 | # with this program. If not, see . 43 | # 44 | # As a special exception, the respective Autoconf Macro's copyright owner 45 | # gives unlimited permission to copy, distribute and modify the configure 46 | # scripts that are the output of Autoconf when processing the Macro. You 47 | # need not follow the terms of the GNU General Public License when using 48 | # or distributing such scripts, even though portions of the text of the 49 | # Macro appear in them. The GNU General Public License (GPL) does govern 50 | # all other use of the material that constitutes the Autoconf Macro. 51 | # 52 | # This special exception to the GPL applies to versions of the Autoconf 53 | # Macro released by the Autoconf Archive. When you make and distribute a 54 | # modified version of the Autoconf Macro, you may extend this special 55 | # exception to the GPL to apply to your modified version as well. 56 | 57 | #serial 3 58 | 59 | AC_DEFUN([AX_VAR_PUSHVALUE],[ 60 | increment([$1_counter]) 61 | 62 | AS_VAR_PUSHDEF([variable],[$1]) dnl 63 | AS_VAR_PUSHDEF([backup],[save_$1_]$1_counter) dnl 64 | 65 | AS_VAR_SET(backup,$variable) dnl 66 | AS_VAR_SET(variable,["]m4_default($2,$variable)["]) dnl 67 | 68 | AS_VAR_POPDEF([variable]) dnl 69 | AS_VAR_POPDEF([backup]) dnl 70 | ])dnl AX_PUSH_VAR 71 | 72 | AC_DEFUN([AX_VAR_POPVALUE],[ 73 | AS_VAR_PUSHDEF([variable],[$1]) dnl 74 | AS_VAR_PUSHDEF([backup],[save_$1_]$1_counter) dnl 75 | 76 | AS_VAR_SET(variable,$backup) dnl 77 | 78 | decrement([$1_counter]) 79 | AS_VAR_POPDEF([variable]) dnl 80 | AS_VAR_POPDEF([backup]) dnl 81 | ])dnl AX_POP_VAR 82 | 83 | # ------------------------- 84 | # Auxiliary macro 85 | # ------------------------- 86 | # increment(counter_name) 87 | # 88 | # Increment the value of a named counter. 89 | # Initialize to 1 if not defined 90 | # ------------------------- 91 | m4_define([increment],[dnl 92 | m4_ifdef([$1],dnl 93 | [m4_define([$1],m4_incr($1))],dnl 94 | [m4_define([$1],[1])]dnl 95 | )dnl 96 | ])dnl 97 | 98 | # ------------------------- 99 | # Auxiliary macro 100 | # ------------------------- 101 | # decrement(counter_name) 102 | # 103 | # Decrement the value of a named counter. 104 | # Throws an error if counter not defined 105 | # or value reaches zero. 106 | # ------------------------- 107 | m4_define([decrement],[dnl 108 | m4_ifdef([$1],dnl 109 | [m4_if(m4_eval($1 > 0), 110 | [1],m4_define([$1],m4_decr($1)),dnl 111 | [m4_fatal([Missing call to AX_VAR_PUSHVALUE with var $1])]dnl 112 | )],dnl 113 | [m4_fatal([Missing call to AX_VAR_PUSHVALUE with var $1])])dnl 114 | ])dnl 115 | -------------------------------------------------------------------------------- /m4/devkitpro.m4: -------------------------------------------------------------------------------- 1 | # -*- mode: autoconf -*- 2 | # devkitpro.m4 - Macros to handle devkitPro setup. 3 | # URL: https://github.com/dkosmari/devkitpro-autoconf/ 4 | 5 | # Copyright (c) 2025 Daniel K. O. 6 | # 7 | # Copying and distribution of this file, with or without modification, are permitted in 8 | # any medium without royalty provided the copyright notice and this notice are 9 | # preserved. This file is offered as-is, without any warranty. 10 | 11 | #serial 4 12 | 13 | # DEVKITPRO_INIT 14 | # -------------- 15 | # 16 | # This macro sets up base devkitPro variables to be used by other macros. The option 17 | # `--with-devkitpro=' is also processed, to override the `DEVKITPRO' variable. 18 | # 19 | # Output variables: 20 | # - `DEVKITPRO': path to devkitPro. 21 | # - `DEVKITPRO_PORTLIBS': path to portlibs. 22 | # 23 | # The file `aminclude.am` is generated with extra Makefile rules: 24 | # - Add `@INC_AMINCLUDE@` to the Makefile that needs them. 25 | # - Add `DISTCLEANFILES = $(AMINCLUDE)' to the toplevel `Makefile.am` to remove this 26 | # file during `make distclean'. 27 | 28 | AC_DEFUN([DEVKITPRO_INIT], [ 29 | 30 | AC_REQUIRE([AC_CANONICAL_HOST]) 31 | 32 | # Make sure macros that look up programs don't appear before this, since we may need 33 | # to adjust PATH. 34 | AC_BEFORE([$0], [AM_INIT_AUTOMAKE]) 35 | # specific program tests 36 | AC_BEFORE([$0], [AC_PROG_CC]) 37 | AC_BEFORE([$0], [AC_PROG_CXX]) 38 | AC_BEFORE([$0], [AC_PROG_CPP]) 39 | AC_BEFORE([$0], [AC_PROG_RANLIB]) 40 | # automake also has these 41 | AC_BEFORE([$0], [AM_PROG_AR]) 42 | AC_BEFORE([$0], [AM_PROG_AS]) 43 | # cross-compilation tool tests 44 | AC_BEFORE([$0], [AC_CHECK_TOOL]) 45 | AC_BEFORE([$0], [AC_CHECK_TOOLS]) 46 | AC_BEFORE([$0], [AC_PATH_TARGET_TOOL]) 47 | AC_BEFORE([$0], [AC_PATH_TOOL]) 48 | AC_BEFORE([$0], [PKG_PROG_PKG_CONFIG]) 49 | 50 | AC_ARG_VAR([DEVKITPRO], [path to devkitPro]) 51 | 52 | # --with-devkitpro can override DEVKITPRO variable 53 | AC_ARG_WITH([devkitpro], 54 | [AS_HELP_STRING([--with-devkitpro=PATH-TO-DEVKITPRO], 55 | [Set the base path to devkitPro. This overrides the variable DEVKITPRO])], 56 | [AS_VAR_SET([DEVKITPRO], [$withval])]) 57 | 58 | # check if DEVKITPRO is set 59 | AC_MSG_CHECKING([devkitPro path]) 60 | 61 | AS_VAR_SET_IF([DEVKITPRO], 62 | [ 63 | AC_MSG_RESULT([$DEVKITPRO]) 64 | ], 65 | [ 66 | AC_MSG_RESULT([not found]) 67 | AC_MSG_ERROR([need --with-devkitpro=PATH-TO-DEVKITPRO or DEVKITPRO=PATH-TO-DEVKITPRO]) 68 | ]) 69 | 70 | AC_SUBST([DEVKITPRO]) 71 | 72 | # set DEVKITPRO_PORTLIBS 73 | AC_ARG_VAR([DEVKITPRO_PORTLIBS], [path to portlibs]) 74 | AS_VAR_SET([DEVKITPRO_PORTLIBS], [$DEVKITPRO/portlibs]) 75 | AC_SUBST([DEVKITPRO_PORTLIBS]) 76 | 77 | # custom Makefile recipes 78 | AX_ADD_AM_MACRO([ 79 | clean: clean-strip-elf 80 | .PHONY: clean-strip-elf 81 | clean-strip-elf:; \$(RM) *.strip.elf 82 | %.strip.elf: %.elf; \$(STRIP) -g \$< -o \$[@] 83 | ]) 84 | 85 | # make updated PATH visible to Makefiles 86 | AC_ARG_VAR([PATH]) 87 | AC_SUBST([PATH]) 88 | 89 | ])dnl DEVKITPRO_INIT 90 | 91 | 92 | # DEVKITPRO_APPEND_PATH(SEARCH-PATH) 93 | # ---------------------------------- 94 | # 95 | # Appends SEARCH-PATH to PATH if it's not there already. 96 | # 97 | # Output variables: 98 | # - `PATH' 99 | 100 | AC_DEFUN([DEVKITPRO_APPEND_PATH], [ 101 | 102 | if @<:@@<:@ ":@S|@PATH:" != *":$1:"* @:>@@:>@ 103 | then 104 | AS_VAR_APPEND([PATH], [":$1"]) 105 | else 106 | AC_MSG_RESULT([yes]) 107 | fi 108 | 109 | ])dnl DEVKITPRO_APPEND_PATH 110 | 111 | 112 | # DEVKITPRO_APPEND_TOOL_PATH(EXECUTABLE, SEARCH-PATH) 113 | # --------------------------------------------------- 114 | # 115 | # This macro checks if an executable is found in PATH; if not, appends SEARCH-PATH to PATH. 116 | # 117 | # Output variables: 118 | # - `PATH' 119 | 120 | AC_DEFUN([DEVKITPRO_APPEND_TOOL_PATH], [ 121 | AC_MSG_CHECKING([if $2 is in PATH]) 122 | AS_IF([! which $1 1>/dev/null 2>/dev/null], 123 | [ 124 | AC_MSG_RESULT([no, will append $2 to PATH]) 125 | DEVKITPRO_APPEND_PATH([$2]) 126 | # Test if it's usable. 127 | AS_IF([! which $1 1>/dev/null 2>/dev/null], 128 | [AC_MSG_ERROR([could not execute $1])]) 129 | ], 130 | [AC_MSG_RESULT([yes])]) 131 | ])dnl DEVKITPRO_APPEND_TOOL_PATH 132 | 133 | 134 | # DEVKITPRO_CHECK_LIBRARY(HEADER-FILE, 135 | # LIBRARY-FILE, 136 | # CPPFLAGS, 137 | # LDFLAGS, 138 | # ACTION-IF-FOUND, 139 | # ACTION-IF-NOT-FOUND) 140 | # -------------------------------------------- 141 | # 142 | # Similar to AX_CHECK_LIBRARY, but does not create special variables. 143 | 144 | AC_DEFUN([DEVKITPRO_CHECK_LIBRARY], [ 145 | 146 | AX_VAR_PUSHVALUE([CPPFLAGS], [$CPPFLAGS $3]) 147 | AX_VAR_PUSHVALUE([LDFLAGS], [$LDFLAGS $4]) 148 | 149 | AC_CHECK_HEADER([$1], 150 | [ 151 | AC_CHECK_LIB([$2], 152 | [main], 153 | [_FOUND_LIB=yes], 154 | [_FOUND_LIB=no]) 155 | ], 156 | [_FOUND_LIB=no]) 157 | 158 | AX_VAR_POPVALUE([LDFLAGS]) 159 | AX_VAR_POPVALUE([CPPFLAGS]) 160 | 161 | AS_VAR_IF([_FOUND_LIB], [yes], 162 | [$5], 163 | [$6]) 164 | 165 | AS_UNSET([_FOUND_LIB]) 166 | 167 | ])dnl DEVKITPRO_CHECK_LIBRARY 168 | 169 | 170 | # DEVKITPRO_CHECK_LIBRARY_FULL(PREFIX, 171 | # HEADER-FILE, 172 | # LIBRARY-FILE, 173 | # CPPFLAGS, 174 | # LDFLAGS, 175 | # ACTION-IF-FOUND, 176 | # ACTION-IF-NOT-FOUND) 177 | #-------------------------------------------------- 178 | # 179 | # Similar to AX_CHECK_LIBRARY, but generates _CPPFLAGS, _LDFLAGS and _LIBS 180 | 181 | AC_DEFUN([DEVKITPRO_CHECK_LIBRARY_FULL], [ 182 | 183 | AC_ARG_VAR($1[_CPPFLAGS], [C preprocessor flags for ]$1[ headers]) 184 | AC_ARG_VAR($1[_LDFLAGS], [linker flags for ]$1[ libraries]) 185 | AC_ARG_VAR($1[_LIBS], [library link option for ]$1[ libraries]) 186 | 187 | AC_CACHE_VAL(AS_TR_SH([dkp_cv_have_]$1), 188 | [ 189 | AX_VAR_PUSHVALUE([CPPFLAGS], [$CPPFLAGS $4]) 190 | AX_VAR_PUSHVALUE([LDFLAGS], [$LDFLAGS $5]) 191 | 192 | AC_CHECK_HEADER($2, 193 | [ 194 | AC_CHECK_LIB($3, 195 | [main], 196 | [AS_TR_SH([dkp_cv_have_]$1)=yes], 197 | [AS_TR_SH([dkp_cv_have_]$1)=no]) 198 | ], 199 | [AS_TR_SH([dkp_cv_have_]$1)=no]) 200 | 201 | AX_VAR_POPVALUE([LDFLAGS]) 202 | AX_VAR_POPVALUE([CPPFLAGS]) 203 | ]) 204 | 205 | AS_IF([test "$]AS_TR_SH([dkp_cv_have_]$1)[" = "yes"], 206 | [ 207 | AC_DEFINE([HAVE_]$1, [1], [Define to 1 if ]$1[ is found]) 208 | AS_VAR_SET($1[_LIBS], ["-l$3"]) 209 | AS_VAR_SET($1[_CPPFLAGS], ["$4"]) 210 | AS_VAR_SET($1[_LDFLAGS], ["$5"]) 211 | [$6] 212 | ], 213 | [$7]) 214 | 215 | ])dnl DEVKITPRO_CHECK_LIBRARY_FULL 216 | 217 | 218 | -------------------------------------------------------------------------------- /m4/devkitpro_ppc.m4: -------------------------------------------------------------------------------- 1 | # -*- mode: autoconf -*- 2 | # devkitpro_ppc.m4 - Macros to handle PPC toolchains. 3 | # URL: https://github.com/dkosmari/devkitpro-autoconf/ 4 | 5 | # Copyright (c) 2025 Daniel K. O. 6 | # 7 | # Copying and distribution of this file, with or without modification, are permitted in 8 | # any medium without royalty provided the copyright notice and this notice are 9 | # preserved. This file is offered as-is, without any warranty. 10 | 11 | #serial 3 12 | 13 | # DEVKITPRO_PPC_INIT 14 | # ------------------ 15 | # 16 | # This macro adjuts the environment for PPC-based targets. It must be called before 17 | # `AM_INIT_AUTOMAKE', and before any cross-compilation tool is checked. 18 | # 19 | # Output variables: 20 | # - `DEVKITPPC': sets path to devkitPPC 21 | # - `PATH': appends `:$DEVKITPPC/bin' if necessary. 22 | # - `DEVKITPRO_PORTLIBS_PPC': sets path to portlibs/ppc 23 | 24 | AC_DEFUN([DEVKITPRO_PPC_INIT],[ 25 | 26 | DEVKITPRO_INIT 27 | 28 | # Sanity check for host type. 29 | AS_CASE($host, 30 | [powerpc-*-eabi], [], 31 | [AC_MSG_ERROR([invalid host ($host), you should use --host=powerpc-eabi])]) 32 | 33 | 34 | # set DEVKITPPC 35 | AC_ARG_VAR([DEVKITPPC], [path to devkitPPC]) 36 | # if not set, set it to $DEVKITPRO/devkitPPC 37 | AS_VAR_SET_IF([[DEVKITPPC]], 38 | [], 39 | [AS_VAR_SET([DEVKITPPC], [$DEVKITPRO/devkitPPC])]) 40 | AC_SUBST([DEVKITPPC]) 41 | 42 | # See if we can find cross tools in PATH already; if not, append $DEVKITPPC/bin to 43 | # PATH 44 | DEVKITPRO_APPEND_TOOL_PATH([powerpc-eabi-nm], [$DEVKITPPC/bin]) 45 | 46 | # Now check that DEVKITPPC/bin binaries are usable 47 | AS_IF([! which powerpc-eabi-nm 1>/dev/null 2>/dev/null], 48 | [AC_MSG_ERROR([devkitPPC binaries not found in PATH=$PATH])]) 49 | 50 | AC_ARG_VAR([DEVKITPRO_PORTLIBS_PPC], [path to portlibs/ppc]) 51 | AS_VAR_SET([DEVKITPRO_PORTLIBS_PPC], [$DEVKITPRO_PORTLIBS/ppc]) 52 | AC_SUBST([DEVKITPRO_PORTLIBS_PPC]) 53 | 54 | ])dnl DEVKITPRO_PPC_INIT 55 | 56 | 57 | # DEVKITPRO_PPC_SETUP 58 | # ------------------- 59 | # 60 | # This macro adjuts CPPFLAGS and LIBS to use portlibs libraries. 61 | # Call this after automake and tools check, to allow automake to set default 62 | # CFLAGS/CXXFLAGS. 63 | # 64 | # Output variables: 65 | # - `CPPFLAGS': prepends search path for portlibs/ppc/include 66 | # - `LIBS': prepends search path for portlibs/ppc/lib 67 | 68 | AC_DEFUN([DEVKITPRO_PPC_SETUP],[ 69 | 70 | AS_VAR_SET_IF([DEVKITPRO_PORTLIBS_PPC], 71 | [], 72 | [AC_MSG_ERROR([DEVKITPRO_PORTLIBS_PPC not defined!])]) 73 | 74 | AX_PREPEND_FLAG([-I$DEVKITPRO_PORTLIBS_PPC/include], [CPPFLAGS]) 75 | 76 | AX_PREPEND_FLAG([-L$DEVKITPRO_PORTLIBS_PPC/lib], [LIBS]) 77 | 78 | ])dnl DEVKITPRO_PPC_SETUP 79 | -------------------------------------------------------------------------------- /m4/devkitpro_wut.m4: -------------------------------------------------------------------------------- 1 | # -*- mode: autoconf -*- 2 | # devkitpro_wut.m4 - Macros to handle WUT setup. 3 | # URL: https://github.com/dkosmari/devkitpro-autoconf/ 4 | 5 | # Copyright (c) 2025 Daniel K. O. 6 | # 7 | # Copying and distribution of this file, with or without modification, are permitted in 8 | # any medium without royalty provided the copyright notice and this notice are 9 | # preserved. This file is offered as-is, without any warranty. 10 | 11 | #serial 6 12 | 13 | # DEVKITPRO_WUT_INIT 14 | # ------------------ 15 | # 16 | # This macro adjusts paths for Wii U homebrew, using WUT. 17 | # Call this before `AM_INIT_AUTOMAKE`. It will call DEVKITPRO_PPC_INIT. 18 | # 19 | # Output variables: 20 | # - `ELF2RPL': set to `elf2rpl' binary. 21 | # - `PATH': appends tools and portlibs paths. 22 | # - `WUUHBTOOL': set to `wuhbtool' binary. 23 | # - `WUT_ROOT': set to `DEVKITPRO/wut' 24 | 25 | AC_DEFUN([DEVKITPRO_WUT_INIT],[ 26 | 27 | DEVKITPRO_PPC_INIT 28 | 29 | # Ensure $DEVKITPRO/tools/bin is in PATH 30 | DEVKITPRO_APPEND_TOOL_PATH([elf2rpl], [$DEVKITPRO/tools/bin]) 31 | 32 | AC_CHECK_PROGS([ELF2RPL], [elf2rpl]) 33 | AC_CHECK_PROGS([WUHBTOOL], [wuhbtool]) 34 | 35 | # set DEVKITPRO_PORTLIBS_WIIU 36 | AC_ARG_VAR([DEVKITPRO_PORTLIBS_WIIU], [path to portlibs/wiiu]) 37 | AS_VAR_SET([DEVKITPRO_PORTLIBS_WIIU], [$DEVKITPRO_PORTLIBS/wiiu]) 38 | AC_SUBST([DEVKITPRO_PORTLIBS_WIIU]) 39 | 40 | # Append portlibs/wiiu/bin and portlibs/ppc/bin to PATH 41 | # Note: we don't know if any portlibs package is installed or even needed. 42 | DEVKITPRO_APPEND_PATH([$DEVKITPRO_PORTLIBS_WIIU/bin]) 43 | DEVKITPRO_APPEND_PATH([$DEVKITPRO_PORTLIBS_PPC/bin]) 44 | 45 | # set WUT_ROOT 46 | AC_ARG_VAR([WUT_ROOT], [path to wut]) 47 | AS_VAR_SET([WUT_ROOT], [$DEVKITPRO/wut]) 48 | AC_SUBST([WUT_ROOT]) 49 | 50 | ]) 51 | 52 | 53 | # DEVKITPRO_WUT_OPT_INIT 54 | # ---------------------- 55 | # 56 | # Calls DEVKITPRO_WUT_INIT only if `--enable-wiiu' argument is given. 57 | 58 | AC_DEFUN([DEVKITPRO_WUT_OPT_INIT],[ 59 | 60 | AC_ARG_ENABLE([enable-wiiu], 61 | [AS_HELP_STRING([--enable-wiiu], [build Wii U homebrew])]) 62 | 63 | AS_VAR_IF([enable_wiiu], [yes], [DEVKITPRO_WUT_INIT]) 64 | 65 | ])dnl DEVKITPRO_WUT_OPT_INIT 66 | 67 | 68 | # DEVKITPRO_WUT_SETUP 69 | # ------------------- 70 | # 71 | # This macro adjusts compilation flags for Wii U homebrew, using WUT. 72 | # 73 | # Output variables: 74 | # - `CFLAGS' 75 | # - `CPPFLAGS' 76 | # - `CXXFLAGS' 77 | # - `LDFLAGS' 78 | # - `LIBS' 79 | 80 | AC_DEFUN([DEVKITPRO_WUT_SETUP],[ 81 | 82 | AS_VAR_SET_IF([WUT_ROOT], [], [AC_MSG_ERROR([WUT_ROOT not set.])]) 83 | 84 | DEVKITPRO_PPC_SETUP 85 | 86 | AX_PREPEND_FLAG([-D__WIIU__], [CPPFLAGS]) 87 | AX_PREPEND_FLAG([-D__WUT__], [CPPFLAGS]) 88 | AX_PREPEND_FLAG([-I$DEVKITPRO_PORTLIBS_WIIU/include], [CPPFLAGS]) 89 | AX_PREPEND_FLAG([-I$WUT_ROOT/usr/include], [CPPFLAGS]) 90 | AX_PREPEND_FLAG([-I$WUT_ROOT/include], [CPPFLAGS]) 91 | 92 | AX_PREPEND_FLAG([-mcpu=750], [CFLAGS]) 93 | AX_PREPEND_FLAG([-meabi], [CFLAGS]) 94 | AX_PREPEND_FLAG([-mhard-float], [CFLAGS]) 95 | 96 | AX_PREPEND_FLAG([-mcpu=750], [CXXFLAGS]) 97 | AX_PREPEND_FLAG([-meabi], [CXXFLAGS]) 98 | AX_PREPEND_FLAG([-mhard-float], [CXXFLAGS]) 99 | 100 | AX_PREPEND_FLAG([-L$DEVKITPRO_PORTLIBS_WIIU/lib], [LIBS]) 101 | AX_PREPEND_FLAG([-L$WUT_ROOT/usr/lib], [LIBS]) 102 | AX_PREPEND_FLAG([-L$WUT_ROOT/lib], [LIBS]) 103 | 104 | AX_PREPEND_FLAG([-specs=$WUT_ROOT/share/wut.specs], [LDFLAGS]) 105 | 106 | DEVKITPRO_CHECK_LIBRARY([wut.h], 107 | [wut], 108 | [], 109 | [], 110 | [AX_APPEND_FLAG([-lwut], [LIBS])], 111 | [AC_MSG_ERROR([wut not found in $DEVKITPRO; install the package with "dkp-pacman -S wut"])]) 112 | 113 | # custom Makefile recipes for building RPX 114 | AX_ADD_AM_MACRO([ 115 | clean: clean-rpx 116 | .PHONY: clean-rpx 117 | clean-rpx:; \$(RM) *.rpx 118 | %.rpx: %.strip.elf; \$(ELF2RPL) \$< \$[@] 119 | ]) 120 | 121 | ])dnl DEVKITPRO_WUT_SETUP 122 | 123 | 124 | AC_DEFUN([DEVKITPRO_WUT_SETUP_RPL],[ 125 | 126 | AS_VAR_SET_IF([WUT_ROOT], [], [AC_MSG_ERROR([WUT_ROOT not defined.])]) 127 | 128 | AX_PREPEND_FLAG([-specs=$WUT_ROOT/share/rpl.specs], [LDFLAGS]) 129 | 130 | # custom Makefile recipes for building RPL 131 | AX_ADD_AM_MACRO([ 132 | clean: clean-rpl 133 | .PHONY: clean-rpl 134 | clean-rpl:; \$(RM) *.rpl 135 | %.rpl: %.strip.elf; \$(ELF2RPL) --rpl \$< \$[@] 136 | ]) 137 | 138 | ])dnl DEVKITPRO_WUT_SETUP_RPL 139 | 140 | 141 | 142 | # WIIU_WUT_CHECK_LIBMOCHA([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 143 | # ----------------------------------------------------------------- 144 | # 145 | # Checks for the presence of libmocha. 146 | # 147 | # Output variables: 148 | # - `HAVE_WIIU_WUT_LIBMOCHA' 149 | # - `WIIU_WUT_LIBMOCHA_LIBS' 150 | 151 | AC_DEFUN([WIIU_WUT_CHECK_LIBMOCHA],[ 152 | 153 | AS_VAR_SET_IF([WUT_ROOT], [], [AC_MSG_ERROR([WUT_ROOT not set.])]) 154 | 155 | DEVKITPRO_CHECK_LIBRARY_FULL([WIIU_WUT_LIBMOCHA], 156 | [mocha/mocha.h], 157 | [mocha], 158 | [], 159 | [], 160 | [$1], 161 | m4_default([$2], 162 | [AC_MSG_ERROR([libmocha not found; get it from https://github.com/wiiu-env/libmocha])])) 163 | 164 | ])dnl WIIU_WUT_CHECK_LIBMOCHA 165 | -------------------------------------------------------------------------------- /m4/wiiu_wums.m4: -------------------------------------------------------------------------------- 1 | # -*- mode: autoconf -*- 2 | # wiiu_wums.m4 - Macros to handle Wii U Module System. 3 | # URL: https://github.com/dkosmari/devkitpro-autoconf/ 4 | 5 | # Copyright (c) 2025 Daniel K. O. 6 | # 7 | # Copying and distribution of this file, with or without modification, are permitted in 8 | # any medium without royalty provided the copyright notice and this notice are 9 | # preserved. This file is offered as-is, without any warranty. 10 | 11 | #serial 9 12 | 13 | # WIIU_WUMS_INIT 14 | # -------------- 15 | # 16 | # This macro adjusts the environment to use the Wii U Module System (WUMS). 17 | # 18 | # Output variables: 19 | # - `WIIU_WUMS' 20 | # - `LIBS' 21 | # 22 | # Note: to create a module, see `WIIU_WUMS_MODULE_INIT'. 23 | 24 | AC_DEFUN([WIIU_WUMS_INIT],[ 25 | 26 | AC_REQUIRE([DEVKITPRO_WUT_INIT]) 27 | 28 | # set WIIU_WUMS 29 | AC_ARG_VAR([WIIU_WUMS], [path to wums]) 30 | AS_VAR_SET([WIIU_WUMS], [$DEVKITPRO/wums]) 31 | AC_SUBST([WIIU_WUMS]) 32 | 33 | ])dnl WIIU_WUMS_INIT 34 | 35 | 36 | # WIIU_WUMS_SETUP 37 | # --------------- 38 | # 39 | # This macro adjusts compilation flags to use the Wii U Module System (WUMS). 40 | # 41 | # Output variables: 42 | # - `CPPFLAGS' 43 | # - `LIBS' 44 | # 45 | # Note: to create a module, see `WIIU_WUMS_MODULE_SETUP'. 46 | 47 | AC_DEFUN([WIIU_WUMS_SETUP],[ 48 | 49 | AC_REQUIRE([DEVKITPRO_WUT_SETUP]) 50 | 51 | AS_VAR_SET_IF([WIIU_WUMS], [], [AC_MSG_ERROR([WIIU_WUMS not set])]) 52 | 53 | AX_PREPEND_FLAG([-I$WIIU_WUMS/include], [CPPFLAGS]) 54 | 55 | AX_PREPEND_FLAG([-L$WIIU_WUMS/lib], [LIBS]) 56 | 57 | ])dnl WIIU_WUMS_SETUP 58 | 59 | 60 | # WIIU_WUMS_CHECK_LIBBUTTONCOMBO([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 61 | # ------------------------------------------------------------------------ 62 | # 63 | # Checks for presence of libbuttoncombo. 64 | # 65 | # Output variables: 66 | # - `HAVE_WIIU_WUMS_LIBBUTTONCOMBO' 67 | # - `WIIU_WUMS_LIBBUTTONCOMBO_LIBS' 68 | 69 | AC_DEFUN([WIIU_WUMS_CHECK_LIBBUTTONCOMBO],[ 70 | 71 | DEVKITPRO_CHECK_LIBRARY_FULL([WIIU_WUMS_LIBBUTTONCOMBO], 72 | [buttoncombo/api.h], 73 | [buttoncombo], 74 | [], 75 | [], 76 | [$1], 77 | m4_default([$2], 78 | [AC_MSG_ERROR([libbuttoncombo not found; get it from https://github.com/wiiu-env/libbuttoncombo])])) 79 | 80 | ])dnl WIIU_WUMS_CHECK_LIBBUTTONCOMBO 81 | 82 | 83 | # WIIU_WUMS_CHECK_LIBCURLWRAPPER([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 84 | # ------------------------------------------------------------------------ 85 | # 86 | # Checks for presence of libcurlwrapper. 87 | # 88 | # Output variables: 89 | # - `HAVE_WIIU_WUMS_LIBCURLWRAPPER' 90 | # - `WIIU_WUMS_LIBCURLWRAPPER_LIBS' 91 | 92 | AC_DEFUN([WIIU_WUMS_CHECK_LIBCURLWRAPPER],[ 93 | 94 | DEVKITPRO_CHECK_LIBRARY_FULL([WIIU_WUMS_LIBCURLWRAPPER], 95 | [curl/curl.h], 96 | [curlwrapper], 97 | [], 98 | [], 99 | [$1], 100 | m4_default([$2], 101 | [AC_MSG_ERROR([libcurlwrapper not found; get it from https://github.com/wiiu-env/libcurlwrapper])])) 102 | 103 | ])dnl WIIU_WUMS_CHECK_LIBCURLWRAPPER 104 | 105 | 106 | # WIIU_WUMS_CHECK_LIBFUNCTIONPATCHER([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 107 | # ---------------------------------------------------------------------------- 108 | # 109 | # Checks for presence of libfunctionpatcher. 110 | # 111 | # Output variables: 112 | # - `HAVE_WIIU_WUMS_LIBFUNCTIONPATCHER' 113 | # - `WIIU_WUMS_LIBFUNCTIONPATCHER_LIBS' 114 | 115 | AC_DEFUN([WIIU_WUMS_CHECK_LIBFUNCTIONPATCHER],[ 116 | 117 | DEVKITPRO_CHECK_LIBRARY_FULL([WIIU_WUMS_LIBFUNCTIONPATCHER], 118 | [function_patcher/function_patching.h], 119 | [functionpatcher], 120 | [], 121 | [], 122 | [$1], 123 | m4_default([$2], 124 | [AC_MSG_ERROR([libfunctionpatcher not found; get it from https://github.com/wiiu-env/libfunctionpatcher])])) 125 | 126 | ])dnl WIIU_WUMS_CHECK_LIBFUNCTIONPATCHER 127 | 128 | 129 | # WIIU_WUMS_CHECK_LIBMAPPEDMEMORY([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 130 | # ------------------------------------------------------------------------- 131 | # 132 | # Checks for presence of libmappedmemory. 133 | # 134 | # Output variables: 135 | # - `HAVE_WIIU_WUMS_LIBMAPPEDMEMORY' 136 | # - `WIIU_WUMS_LIBMAPPEDMEMORY_LDFLAGS' 137 | # - `WIIU_WUMS_LIBMAPPEDMEMORY_LIBS' 138 | 139 | AC_DEFUN([WIIU_WUMS_CHECK_LIBMAPPEDMEMORY],[ 140 | 141 | AS_VAR_SET_IF([WIIU_WUMS], [], [AC_MSG_ERROR([WIIU_WUMS not set.])]) 142 | 143 | DEVKITPRO_CHECK_LIBRARY_FULL([WIIU_WUMS_LIBMAPPEDMEMORY], 144 | [memory/mappedmemory.h], 145 | [mappedmemory], 146 | [], 147 | [-T$WIIU_WUMS/share/libmappedmemory.ld], 148 | [$1], 149 | m4_default([$2], 150 | [AC_MSG_ERROR([libmappedmemory not found; get it from https://github.com/wiiu-env/libmappedmemory])])) 151 | 152 | ])dnl WIIU_WUMS_CHECK_LIBMAPPEDMEMORY 153 | 154 | 155 | # WIIU_WUMS_CHECK_LIBNOTIFICATIONS([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 156 | # -------------------------------------------------------------------------- 157 | # 158 | # Checks for presence of libnotifications. 159 | # 160 | # Output variables: 161 | # - `HAVE_WIIU_WUMS_LIBNOTIFICATIONS' 162 | # - `WIIU_WUMS_LIBNOTIFICATIONS_LIBS' 163 | 164 | AC_DEFUN([WIIU_WUMS_CHECK_LIBNOTIFICATIONS],[ 165 | 166 | DEVKITPRO_CHECK_LIBRARY_FULL([WIIU_WUMS_LIBNOTIFICATIONS], 167 | [notifications/notifications.h], 168 | [notifications], 169 | [], 170 | [], 171 | [$1], 172 | m4_default([$2], 173 | [AC_MSG_ERROR([libnotifications not found; get it from https://github.com/wiiu-env/libnotifications])])) 174 | 175 | ])dnl WIIU_WUMS_CHECK_LIBNOTIFICATIONS 176 | 177 | 178 | # WIIU_WUMS_MODULE_INIT 179 | # --------------------- 180 | # 181 | # This macro adjusts the environment to create a Wii U Module System (WUMS) module. 182 | 183 | AC_DEFUN([WIIU_WUMS_MODULE_INIT],[ 184 | 185 | AC_REQUIRE([WIIU_WUMS_INIT]) 186 | 187 | ])dnl WIIU_WUMS_MODULE_INIT 188 | 189 | 190 | # WIIU_WUMS_MODULE_SETUP 191 | # ---------------------- 192 | # 193 | # This macro adjusts the compilation flags to create a Wii U Module System (WUMS) module. 194 | # 195 | # Output variables: 196 | # - `CPPFLAGS' 197 | # - `LDFLAGS' 198 | # - `LIBS' 199 | 200 | AC_DEFUN([WIIU_WUMS_MODULE_SETUP],[ 201 | 202 | AC_REQUIRE([WIIU_WUMS_SETUP]) 203 | 204 | AX_PREPEND_FLAG([-I$WIIU_WUMS/include], [CPPFLAGS]) 205 | 206 | AX_PREPEND_FLAG([-specs=$WIIU_WUMS/share/wums.specs], [LDFLAGS]) 207 | AX_PREPEND_FLAG([-T$WIIU_WUMS/share/wums.ld], [LDFLAGS]) 208 | 209 | AX_PREPEND_FLAG([-L$WIIU_WUMS/lib], [LIBS]) 210 | 211 | # do a compilation test to check for header and lib 212 | DEVKITPRO_CHECK_LIBRARY([wums.h], 213 | [wums], 214 | [], 215 | [], 216 | [AX_PREPEND_FLAG([-lwums], [LIBS])], 217 | [AC_MSG_ERROR([WUMS not found; get it from https://github.com/wiiu-env/WiiUModuleSystem])]) 218 | 219 | # custom Makefile recipes for building .wms modules. 220 | AX_ADD_AM_MACRO([ 221 | clean: clean-wms 222 | .PHONY: clean-wms 223 | clean-wms:; \$(RM) *.wms 224 | %.wms: %.strip.elf; 225 | \$(ELF2RPL) \$< \$[@] 226 | printf '\xAF\xFE' | dd of=\$[@] bs=1 seek=9 count=2 conv=notrunc status=none 227 | ]) 228 | 229 | ])dnl WIIU_WUMS_MODULE_SETUP 230 | -------------------------------------------------------------------------------- /m4/wiiu_wups.m4: -------------------------------------------------------------------------------- 1 | # -*- mode: autoconf -*- 2 | # wiiu_wups.m4 - Macros to handle Wii U Plugin System 3 | # URL: https://github.com/dkosmari/devkitpro-autoconf/ 4 | 5 | # Copyright (c) 2025 Daniel K. O. 6 | # 7 | # Copying and distribution of this file, with or without modification, are permitted in 8 | # any medium without royalty provided the copyright notice and this notice are 9 | # preserved. This file is offered as-is, without any warranty. 10 | 11 | #serial 5 12 | 13 | # WIIU_WUPS_INIT 14 | # -------------- 15 | # 16 | # This macro adjusts the environment for the Wii U Plugin System (WUPS). 17 | # 18 | # Output variables: 19 | # - `WIIU_WUPS' 20 | 21 | AC_DEFUN([WIIU_WUPS_INIT],[ 22 | 23 | AC_REQUIRE([DEVKITPRO_WUT_INIT]) 24 | 25 | # set WIIU_WUPS 26 | AS_VAR_SET([WIIU_WUPS], [$DEVKITPRO/wups]) 27 | 28 | ])dnl WIIU_WUPS_INIT 29 | 30 | 31 | # WIIU_WUPS_SETUP 32 | # --------------- 33 | # 34 | # This macro adjusts compilation flags for the Wii U Plugin System (WUPS). 35 | # 36 | # Output variables: 37 | # - `CPPFLAGS' 38 | # - `LDFLAGS' 39 | # - `LIBS' 40 | 41 | AC_DEFUN([WIIU_WUPS_SETUP],[ 42 | 43 | AC_REQUIRE([DEVKITPRO_WUT_SETUP]) 44 | 45 | AS_VAR_SET_IF([WIIU_WUPS], [], [AC_MSG_ERROR([WIIU_WUPS not set])]) 46 | 47 | AX_PREPEND_FLAG([-D__WUPS__], [CPPFLAGS]) 48 | AX_PREPEND_FLAG([-I$WIIU_WUPS/include], [CPPFLAGS]) 49 | 50 | AX_PREPEND_FLAG([-T$WIIU_WUPS/share/wups.ld], [LDFLAGS]) 51 | AX_PREPEND_FLAG([-specs=$WIIU_WUPS/share/wups.specs], [LDFLAGS]) 52 | 53 | AX_PREPEND_FLAG([-L$WIIU_WUPS/lib], [LIBS]) 54 | 55 | # check for header and lib 56 | DEVKITPRO_CHECK_LIBRARY([wups.h], 57 | [wups], 58 | [], 59 | [], 60 | [AX_PREPEND_FLAG([-lwups], [LIBS])], 61 | [AC_MSG_ERROR([WUPS not found; get it from https://github.com/wiiu-env/WiiUPluginSystem])]) 62 | 63 | 64 | # custom Makefile recipes for building .wps plugins 65 | AX_ADD_AM_MACRO([ 66 | clean: clean-wps 67 | .PHONY: clean-wps 68 | clean-wps:; \$(RM) *.wps 69 | %.wps: %.strip.elf 70 | \$(ELF2RPL) \$< \$[@] 71 | printf 'PL' | dd of=\$[@] bs=1 seek=9 count=2 conv=notrunc status=none 72 | ]) 73 | 74 | ])dnl WIIU_WUPS_SETUP 75 | -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | # src/Makefile.am 2 | 3 | @INC_AMINCLUDE@ 4 | 5 | 6 | AM_CPPFLAGS = \ 7 | $(DEVKITPRO_CPPFLAGS) \ 8 | -I$(top_srcdir)/external/libwupsxx/include 9 | 10 | AM_CXXFLAGS = \ 11 | $(DEVKITPRO_CXXFLAGS) \ 12 | -Wall -Wextra -Werror 13 | 14 | AM_LDFLAGS = $(DEVKITPRO_LDFLAGS) 15 | 16 | LIBS = \ 17 | $(top_builddir)/external/libwupsxx/src/libwupsxx.a \ 18 | $(DEVKITPRO_LIBS) 19 | 20 | 21 | WPS_FILE = papaya-hud.wps 22 | noinst_PROGRAMS = papaya-hud.elf 23 | 24 | papaya_hud_elf_SOURCES = \ 25 | cfg.cpp cfg.hpp \ 26 | coreinit_allocator.h \ 27 | cpu_mon.cpp cpu_mon.hpp \ 28 | fs_mon.cpp fs_mon.hpp \ 29 | gx2_mon.cpp gx2_mon.hpp \ 30 | gx2_perf.h \ 31 | logger.cpp logger.hpp \ 32 | main.cpp \ 33 | net_mon.cpp net_mon.hpp \ 34 | nintendo_glyphs.h \ 35 | overlay.cpp overlay.hpp \ 36 | pad_mon.cpp pad_mon.hpp \ 37 | time_mon.cpp time_mon.hpp \ 38 | utils.cpp utils.hpp 39 | 40 | 41 | all-local: $(WPS_FILE) 42 | 43 | # $(WPS_FILE): papaya-hud.elf 44 | 45 | 46 | install-exec-local: $(WPS_FILE) 47 | curl "ftp://wiiu:/fs/vol/external01/wiiu/environments/aroma/plugins/" --upload-file $(WPS_FILE) 48 | 49 | 50 | uninstall-local: 51 | curl "ftp://wiiu" --quote "DELE /fs/vol/external01/wiiu/environments/aroma/plugins/$(WPS_FILE)" 52 | 53 | 54 | run-local: all 55 | WIILOAD=tcp:wiiu wiiload $(WPS_FILE) 56 | 57 | 58 | .PHONY: company 59 | company: compile_flags.txt 60 | 61 | compile_flags.txt: Makefile 62 | printf "%s" "$(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS)" | xargs -n1 | sort -u > $(srcdir)/compile_flags.txt 63 | $(CPP) -xc++ /dev/null -E -Wp,-v 2>&1 | sed -n 's,^ ,-I,p' >> $(srcdir)/compile_flags.txt 64 | 65 | 66 | CLEANFILES = $(WPS_FILE) 67 | 68 | -------------------------------------------------------------------------------- /src/ax_mon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * Audio Monitoring 11 | */ 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | #include 23 | 24 | #include "ax_mon.hpp" 25 | 26 | #include "cfg.hpp" 27 | 28 | 29 | enum MIXSoundMode { 30 | MIX_SOUND_MODE_MONO = 0, 31 | MIX_SOUND_MODE_STEREO = 1, 32 | MIX_SOUND_MODE_SURROUND = 2, 33 | MIX_SOUND_MODE_5_1 = 4, 34 | }; 35 | 36 | 37 | // TODO: send a PR to WUT 38 | 39 | struct AXProfileInterval { 40 | OSTime start; 41 | OSTime finish; 42 | }; 43 | WUT_CHECK_OFFSET(AXProfileInterval, 0, start); 44 | WUT_CHECK_OFFSET(AXProfileInterval, 8, finish); 45 | WUT_CHECK_SIZE(AXProfileInterval, 16); 46 | 47 | 48 | struct RealAXProfile { 49 | 50 | AXProfileInterval frame; 51 | AXProfileInterval auxCallback; 52 | AXProfileInterval frameCallback; 53 | AXProfileInterval finalMixCallback; 54 | 55 | uint32_t numVoices; 56 | uint32_t numDSPVoices; 57 | 58 | AXProfileInterval DSP; 59 | AXProfileInterval PPC; 60 | 61 | AXProfileInterval post; 62 | 63 | WUT_UNKNOWN_BYTES(8); 64 | OSTime postLatency; // TODO: unused? 65 | 66 | }; 67 | 68 | WUT_CHECK_OFFSET(RealAXProfile, 0, frame); 69 | WUT_CHECK_OFFSET(RealAXProfile, 16, auxCallback); 70 | WUT_CHECK_OFFSET(RealAXProfile, 32, frameCallback); 71 | WUT_CHECK_OFFSET(RealAXProfile, 48, finalMixCallback); 72 | WUT_CHECK_OFFSET(RealAXProfile, 64, numVoices); 73 | WUT_CHECK_OFFSET(RealAXProfile, 68, numDSPVoices); 74 | WUT_CHECK_OFFSET(RealAXProfile, 72, DSP); 75 | WUT_CHECK_OFFSET(RealAXProfile, 88, PPC); 76 | WUT_CHECK_OFFSET(RealAXProfile, 104, post); 77 | WUT_CHECK_OFFSET(RealAXProfile, 128, postLatency); 78 | 79 | WUT_CHECK_SIZE(RealAXProfile, 8 * 17); 80 | 81 | 82 | namespace ax_mon { 83 | 84 | namespace { 85 | 86 | void 87 | print_mode(out_span& out, 88 | AXDeviceMode mode) 89 | { 90 | // TODO: send a PR to WUT for these 91 | switch (mode) { 92 | case 0: 93 | out.append("stereo"); 94 | break; 95 | case 1: // virtual surround on gamepad 96 | out.append("surround"); 97 | break; 98 | case 3: // surround on TV 99 | out.append("5.1"); 100 | break; 101 | case 5: 102 | out.append("mono"); 103 | break; 104 | default: 105 | out.printf("%d", mode); 106 | } 107 | } 108 | 109 | 110 | void* 111 | get_core_func(const char* name) 112 | { 113 | OSDynLoad_Module m; 114 | void* result = nullptr; 115 | 116 | m = nullptr; 117 | if (!OSDynLoad_IsModuleLoaded("sndcore2", &m)) { 118 | if (!OSDynLoad_FindExport(m, 119 | OS_DYNLOAD_EXPORT_FUNC, 120 | name, 121 | &result)) { 122 | // wups::logger::printf("%s found on sndcore2\n", name); 123 | return result; 124 | } 125 | } 126 | 127 | m = nullptr; 128 | if (!OSDynLoad_IsModuleLoaded("snd_core", &m)) { 129 | if (!OSDynLoad_FindExport(m, 130 | OS_DYNLOAD_EXPORT_FUNC, 131 | name, 132 | &result)) 133 | return result; 134 | } 135 | 136 | // wups::logger::printf("%s not found\n", name); 137 | return nullptr; 138 | } 139 | 140 | 141 | using AXIsInit_func_t = BOOL(void); 142 | using AXGetCurrentParams_func_t = void(AXInitParams*); 143 | using AXGetDeviceMode_func_t = AXResult(AXDeviceType, AXDeviceMode*); 144 | 145 | 146 | AXIsInit_func_t* AXIsInit_func; 147 | AXGetCurrentParams_func_t* AXGetCurrentParams_func; 148 | AXGetDeviceMode_func_t* AXGetDeviceMode_func; 149 | 150 | 151 | void 152 | lookup_functions() 153 | { 154 | AXIsInit_func = 155 | reinterpret_cast(get_core_func("AXIsInit")); 156 | AXGetCurrentParams_func = 157 | reinterpret_cast(get_core_func("AXGetCurrentParams")); 158 | AXGetDeviceMode_func = 159 | reinterpret_cast(get_core_func("AXGetDeviceMode")); 160 | } 161 | 162 | 163 | void 164 | clear_functions() 165 | { 166 | AXIsInit_func = nullptr; 167 | AXGetCurrentParams_func = nullptr; 168 | AXGetDeviceMode_func = nullptr; 169 | } 170 | 171 | 172 | std::optional 173 | dyn_AXIsInit() 174 | { 175 | if (!AXIsInit_func) 176 | return {}; 177 | return AXIsInit_func(); 178 | } 179 | 180 | 181 | bool 182 | dyn_AXGetCurrentParams(AXInitParams* params) 183 | { 184 | if (!AXGetCurrentParams_func) 185 | return false; 186 | AXGetCurrentParams_func(params); 187 | return true; 188 | } 189 | 190 | 191 | std::optional 192 | dyn_AXGetDeviceMode(AXDeviceType type, 193 | AXDeviceMode *mode) 194 | { 195 | if (!AXGetDeviceMode_func) 196 | return {}; 197 | return AXGetDeviceMode_func(type, mode); 198 | } 199 | 200 | 201 | struct ax_stats { 202 | 203 | float total_load; 204 | float dsp_load; 205 | float ppc_load; 206 | 207 | unsigned voices; 208 | unsigned dsp_voices; 209 | 210 | }; 211 | 212 | 213 | OSTime 214 | duration(const AXProfileInterval& interval) 215 | { 216 | return interval.finish - interval.start; 217 | } 218 | 219 | 220 | ax_stats 221 | get_stats(RealAXProfile prof) 222 | { 223 | static const float deadline = OSMillisecondsToTicks(3); 224 | 225 | ax_stats result; 226 | 227 | float total = duration(prof.frame); 228 | float dsp_total = duration(prof.DSP); 229 | float ppc_total = 230 | (prof.frameCallback.finish - prof.PPC.start) 231 | + 232 | (prof.frame.finish - prof.post.start); 233 | 234 | result.total_load = total / deadline; 235 | result.dsp_load = dsp_total / deadline; 236 | result.ppc_load = ppc_total / deadline; 237 | 238 | result.voices = prof.numVoices; 239 | result.dsp_voices = prof.numDSPVoices; 240 | 241 | return result; 242 | } 243 | 244 | } // namespace 245 | 246 | 247 | unsigned prof_version = 0; 248 | 249 | RealAXProfile prof_current alignas(0x40); 250 | 251 | 252 | void 253 | initialize() 254 | { 255 | prof_version = 0; 256 | lookup_functions(); 257 | } 258 | 259 | 260 | void 261 | finalize() 262 | { 263 | clear_functions(); 264 | prof_version = 0; 265 | } 266 | 267 | 268 | void 269 | reset() 270 | { 271 | finalize(); 272 | initialize(); 273 | } 274 | 275 | 276 | void 277 | on_application_start() 278 | { 279 | reset(); 280 | } 281 | 282 | 283 | void 284 | get_report(out_span& out, 285 | float) 286 | { 287 | const char* sep = ""; 288 | 289 | auto ax_is_init = dyn_AXIsInit(); 290 | if (!ax_is_init || !*ax_is_init) { 291 | out.append("No audio"); 292 | return; 293 | } 294 | 295 | AXDeviceMode mode; 296 | auto status_mode_tv = dyn_AXGetDeviceMode(AX_DEVICE_TYPE_TV, &mode); 297 | if (status_mode_tv && !*status_mode_tv) { 298 | out.append(sep); 299 | sep = "/"; 300 | print_mode(out, mode); 301 | } 302 | 303 | auto status_mode_drc = dyn_AXGetDeviceMode(AX_DEVICE_TYPE_DRC, &mode); 304 | if (status_mode_drc && !*status_mode_drc) { 305 | out.append(sep); 306 | sep = ", "; 307 | print_mode(out, mode); 308 | } else 309 | sep = ", "; 310 | 311 | alignas(0x40) AXInitParams params{}; 312 | dyn_AXGetCurrentParams(¶ms); 313 | out.append(sep); 314 | sep = ", "; 315 | switch (params.renderer) { 316 | case AX_INIT_RENDERER_32KHZ: 317 | out.append("32k㎐"); 318 | break; 319 | case AX_INIT_RENDERER_48KHZ: 320 | out.append("48k㎐"); 321 | break; 322 | default: 323 | out.append("unknown"); 324 | } 325 | switch (params.pipeline) { 326 | case AX_INIT_PIPELINE_SINGLE: 327 | // out.append("(1)"); 328 | break; 329 | case AX_INIT_PIPELINE_FOUR_STAGE: 330 | out.append("(4)"); 331 | break; 332 | default: 333 | out.append("(?)"); 334 | } 335 | 336 | if (cfg::audio_busy.value && prof_version) { 337 | auto stats = get_stats(prof_current); 338 | prof_version = 0; 339 | out.append(sep); 340 | sep = ", "; 341 | out.printf("load: %2.1f%% / %2.1f%% / %2.1f%%, voices: %u/%u", 342 | 100.0f * stats.dsp_load, 343 | 100.0f * stats.ppc_load, 344 | 100.0f * stats.total_load, 345 | stats.dsp_voices, 346 | stats.voices); 347 | } 348 | } 349 | 350 | 351 | DECL_FUNCTION(uint32_t, AXGetSwapProfile1, 352 | RealAXProfile* buf, 353 | uint32_t count) 354 | { 355 | uint32_t result = real_AXGetSwapProfile1(buf, count); 356 | if (result > 0 357 | && cfg::enabled.value 358 | && cfg::audio.value 359 | && cfg::audio_busy.value) { 360 | prof_current = buf[result - 1]; 361 | prof_version = 1; 362 | } 363 | return result; 364 | } 365 | 366 | WUPS_MUST_REPLACE(AXGetSwapProfile1, 367 | WUPS_LOADER_LIBRARY_SND_CORE, 368 | AXGetSwapProfile); 369 | 370 | 371 | DECL_FUNCTION(uint32_t, AXGetSwapProfile2, 372 | RealAXProfile* buf, 373 | uint32_t count) 374 | { 375 | uint32_t result = real_AXGetSwapProfile2(buf, count); 376 | if (result > 0 377 | && cfg::enabled.value 378 | && cfg::audio.value 379 | && cfg::audio_busy.value) { 380 | prof_current = buf[result - 1]; 381 | prof_version = 2; 382 | } 383 | return result; 384 | } 385 | 386 | WUPS_MUST_REPLACE(AXGetSwapProfile2, 387 | WUPS_LOADER_LIBRARY_SNDCORE2, 388 | AXGetSwapProfile); 389 | 390 | } // namespace ax_mon 391 | -------------------------------------------------------------------------------- /src/ax_mon.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef AX_MON_HPP 10 | #define AX_MON_HPP 11 | 12 | #include "out_span.hpp" 13 | 14 | namespace ax_mon { 15 | 16 | void 17 | initialize(); 18 | 19 | void 20 | finalize(); 21 | 22 | void 23 | reset(); 24 | 25 | void 26 | on_application_start(); 27 | 28 | void 29 | get_report(out_span& out, 30 | float dt); 31 | 32 | } // namespace ax_mon 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/cfg.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * Configuration 11 | * 12 | * This is where all configuration options are handled. 13 | */ 14 | 15 | #include // OSMemoryBarrier() 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "cfg.hpp" 29 | 30 | #include "overlay.hpp" 31 | 32 | 33 | #ifdef HAVE_CONFIG_H 34 | #include 35 | #endif 36 | 37 | 38 | namespace cfg { 39 | 40 | namespace logger = wups::logger; 41 | 42 | using std::chrono::milliseconds; 43 | using wups::shortcut::combo; 44 | using wups::color; 45 | 46 | using namespace std::literals; 47 | 48 | 49 | // Note: keep the same order as the UI. 50 | 51 | WUPSXX_OPTION("Enabled", 52 | bool, enabled, true); 53 | 54 | WUPSXX_OPTION(" └ Toggle shortcut", 55 | combo, toggle_shortcut, 56 | combo::from_vpad(VPAD_BUTTON_TV | VPAD_BUTTON_L)); 57 | 58 | WUPSXX_OPTION("Time", 59 | bool, time, true); 60 | 61 | WUPSXX_OPTION(" └ Format", 62 | bool, time_24h, true); 63 | 64 | WUPSXX_OPTION("System uptime", 65 | bool, uptime, true); 66 | 67 | WUPSXX_OPTION("Play time", 68 | bool, play_time, true); 69 | 70 | WUPSXX_OPTION("Frames per second", 71 | bool, gpu_fps, true); 72 | 73 | WUPSXX_OPTION("Screen resolution", 74 | bool, gpu_resolution, true); 75 | 76 | WUPSXX_OPTION("GPU utilization ", 77 | bool, gpu_busy, false); 78 | 79 | WUPSXX_OPTION(" └ Show percentage", 80 | bool, gpu_busy_percent, true); 81 | 82 | WUPSXX_OPTION("CPU utilization", 83 | bool, cpu_busy, true); 84 | 85 | WUPSXX_OPTION(" └ Show percentage", 86 | bool, cpu_busy_percent, true); 87 | 88 | WUPSXX_OPTION(" └ Also show ARM CPU ", 89 | bool, cpu_busy_arm, true); 90 | 91 | WUPSXX_OPTION("Network configuration", 92 | bool, net_cfg, false); 93 | 94 | WUPSXX_OPTION("Network bandwidth", 95 | bool, net_bw, true); 96 | 97 | WUPSXX_OPTION(" └ Combine upload and download values", 98 | bool, net_bw_combined, true); 99 | 100 | WUPSXX_OPTION("Filesystem performance", 101 | bool, fs_perf, true); 102 | 103 | WUPSXX_OPTION(" └ Combine read and write values", 104 | bool, fs_perf_combined, true); 105 | 106 | WUPSXX_OPTION("Audio details", 107 | bool, audio, true); 108 | 109 | WUPSXX_OPTION(" └ Audio utilization ", 110 | bool, audio_busy, false); 111 | 112 | WUPSXX_OPTION("Button presses per second", 113 | bool, button_rate, true); 114 | 115 | WUPSXX_OPTION("Battery levels", 116 | bool, battery, true); 117 | 118 | WUPSXX_OPTION(" └ Show percentage", 119 | bool, battery_percent, false); 120 | 121 | WUPSXX_OPTION("Foreground color", 122 | color, color_fg, color(0x60, 0xff, 0x60)); 123 | 124 | WUPSXX_OPTION("Background color", 125 | color, color_bg, color(0x00, 0x00, 0x00, 0xc0)); 126 | 127 | WUPSXX_OPTION("Update interval", 128 | milliseconds, interval, 1s, 100ms, 5s); 129 | 130 | 131 | const std::vector all_options{ 132 | &enabled, 133 | &toggle_shortcut, 134 | &time, 135 | &time_24h, 136 | &uptime, 137 | &play_time, 138 | &gpu_fps, 139 | &gpu_resolution, 140 | &gpu_busy, 141 | &gpu_busy_percent, 142 | &cpu_busy, 143 | &cpu_busy_percent, 144 | &cpu_busy_arm, 145 | &net_cfg, 146 | &net_bw, 147 | &net_bw_combined, 148 | &fs_perf, 149 | &fs_perf_combined, 150 | &audio, 151 | &audio_busy, 152 | &button_rate, 153 | &battery, 154 | &battery_percent, 155 | &color_fg, 156 | &color_bg, 157 | &interval, 158 | }; 159 | 160 | 161 | wups::shortcut::handle toggle_shortcut_handle; 162 | 163 | void 164 | menu_open(wups::category& root) 165 | { 166 | using wups::make_item; 167 | 168 | root.add(make_item(enabled, "yes", "no")); 169 | root.add(make_item(toggle_shortcut, toggle_shortcut_handle)); 170 | root.add(make_item(time, "on", "off")); 171 | root.add(make_item(time_24h, "24h", "12h")); 172 | root.add(make_item(uptime, "on", "off")); 173 | root.add(make_item(play_time, "on", "off")); 174 | root.add(make_item(gpu_fps, "on", "off")); 175 | root.add(make_item(gpu_resolution, "on", "off")); 176 | root.add(make_item(gpu_busy, "on", "off")); 177 | root.add(make_item(gpu_busy_percent, "on", "off")); 178 | root.add(make_item(cpu_busy, "on", "off")); 179 | root.add(make_item(cpu_busy_percent, "on", "off")); 180 | root.add(make_item(cpu_busy_arm, "on", "off")); 181 | root.add(make_item(net_cfg, "on", "off")); 182 | root.add(make_item(net_bw, "on", "off")); 183 | root.add(make_item(net_bw_combined, "on", "off")); 184 | root.add(make_item(fs_perf, "on", "off")); 185 | root.add(make_item(fs_perf_combined, "on", "off")); 186 | root.add(make_item(audio, "on", "off")); 187 | root.add(make_item(audio_busy, "on", "off")); 188 | root.add(make_item(button_rate, "on", "off")); 189 | root.add(make_item(battery, "on", "off")); 190 | root.add(make_item(battery_percent, "on", "off")); 191 | root.add(make_item(color_fg, false)); 192 | root.add(make_item(color_bg, true)); 193 | root.add(make_item(interval, 100ms)); 194 | } 195 | 196 | 197 | void 198 | menu_close() 199 | { 200 | cfg::save(); 201 | 202 | // Note: FS monitoring might run in other threads. 203 | OSMemoryBarrier(); 204 | 205 | if (enabled.value) 206 | overlay::create_or_reset(); 207 | else 208 | overlay::destroy(); 209 | } 210 | 211 | 212 | void 213 | initialize() 214 | { 215 | try { 216 | wups::init(PACKAGE_NAME, menu_open, menu_close); 217 | } 218 | catch (std::exception& e) { 219 | logger::printf("Error initializing config API: %s\n", e.what()); 220 | } 221 | 222 | logger::printf("calling cfg::load()\n"); 223 | load(); 224 | 225 | try { 226 | logger::printf("creating button combo\n"); 227 | auto [h, conflict] = wups::shortcut::create("Toggle HUD", 228 | toggle_shortcut.value, 229 | [](wups::shortcut::ctr_set, 230 | wups::shortcut::handle) 231 | { 232 | overlay::toggle(); 233 | }); 234 | toggle_shortcut_handle = h; 235 | if (conflict) 236 | logger::printf("Shortcut has conflict\n"); 237 | } 238 | catch (std::exception& e) { 239 | logger::printf("Error setting up button combo: %s\n", e.what()); 240 | } 241 | 242 | } 243 | 244 | 245 | void 246 | finalize() 247 | { 248 | wups::shortcut::destroy(toggle_shortcut_handle); 249 | } 250 | 251 | 252 | void 253 | load() 254 | { 255 | for (auto& opt : all_options) 256 | opt->load(); 257 | } 258 | 259 | 260 | void 261 | save() 262 | { 263 | try { 264 | for (const auto& opt : all_options) 265 | opt->store(); 266 | wups::save(); 267 | } 268 | catch (std::exception& e) { 269 | logger::printf("Error saving config: %s\n", e.what()); 270 | } 271 | } 272 | 273 | } // namespace cfg 274 | -------------------------------------------------------------------------------- /src/cfg.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef CFG_HPP 10 | #define CFG_HPP 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | 18 | namespace cfg { 19 | 20 | extern wups::option audio; 21 | extern wups::option audio_busy; 22 | extern wups::option battery; 23 | extern wups::option battery_percent; 24 | extern wups::option button_rate; 25 | extern wups::option color_bg; 26 | extern wups::option color_fg; 27 | extern wups::option cpu_busy; 28 | extern wups::option cpu_busy_arm; 29 | extern wups::option cpu_busy_percent; 30 | extern wups::option enabled; 31 | extern wups::option fs_perf; 32 | extern wups::option fs_perf_combined; 33 | extern wups::option gpu_busy; 34 | extern wups::option gpu_busy_percent; 35 | extern wups::option gpu_fps; 36 | extern wups::option gpu_resolution; 37 | extern wups::option interval; 38 | extern wups::option net_bw; 39 | extern wups::option net_bw_combined; 40 | extern wups::option net_cfg; 41 | extern wups::option play_time; 42 | extern wups::option time; 43 | extern wups::option time; 44 | extern wups::option time_24h; 45 | extern wups::option uptime; 46 | 47 | void 48 | initialize(); 49 | 50 | void 51 | finalize(); 52 | 53 | void 54 | load(); 55 | 56 | void 57 | save(); 58 | 59 | } // namespace cfg 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /src/cpu_mon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * CPU Monitoring 11 | * 12 | * In this file we take advantage of the leftover "CafeOS Shell" functions left behind 13 | * inside retail coreinit. 14 | * 15 | * For ARM utilization we use the bspRead() function. 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include "cpu_mon.hpp" 28 | 29 | #include "cfg.hpp" 30 | #include "utils.hpp" 31 | 32 | 33 | using std::uint32_t; 34 | using std::size_t; 35 | 36 | 37 | #define AVOID_ALLOCATIONS 38 | 39 | // Define this if you want more accurate ARM CPU usage, but might reduce frame rate. 40 | //#define SYNCHRONOUS_ARM_STAT 41 | 42 | namespace my { 43 | 44 | struct BSPReadRequest { 45 | char entity[32]; 46 | uint32_t instance; 47 | char attribute[32]; 48 | size_t size; 49 | 50 | BSPReadRequest(const char* entity_, 51 | std::uint32_t instance_, 52 | const char* attribute_, 53 | std::size_t size_) 54 | noexcept 55 | { 56 | // std::memset(this, 0, sizeof *this); 57 | std::strncpy(entity, entity_, sizeof entity); 58 | instance = instance_; 59 | std::strncpy(attribute, attribute_, sizeof attribute); 60 | size = size_; 61 | } 62 | }; 63 | static_assert(sizeof(BSPReadRequest) == 0x48); 64 | 65 | 66 | struct BSPReadResponse { 67 | char data[512]; 68 | }; 69 | 70 | 71 | enum BSPCommand { 72 | BSP_CMD_READ = 5, 73 | }; 74 | 75 | 76 | auto bsp_handle_ptr = reinterpret_cast(0x1004f920); 77 | 78 | 79 | /* 80 | * Note: instance argument seems to be 32 bit argument, not 8 bit. 81 | * This version is more efficient than the coreinit bspRead(). 82 | */ 83 | BSPError 84 | bspRead(const char* entity, 85 | uint32_t instance, 86 | const char* attribute, 87 | size_t size, 88 | void* output) 89 | { 90 | if (size > sizeof BSPReadResponse::data) 91 | return BSP_ERROR_SPECIFIED_SIZE_INVALID; 92 | 93 | alignas(0x20) 94 | BSPReadResponse response; 95 | 96 | alignas(0x20) 97 | BSPReadRequest request{ 98 | entity, 99 | instance, 100 | attribute, 101 | size 102 | }; 103 | 104 | auto status = IOS_Ioctl(*bsp_handle_ptr, 105 | BSP_CMD_READ, 106 | &request, sizeof request, 107 | response.data, size); 108 | if (status < 0) { 109 | OSReport("bspRead(): IOS_Ioctl() returned %d\n", status); 110 | return BSP_ERROR_IOS_ERROR; 111 | } 112 | std::memcpy(output, response.data, size); 113 | return BSP_ERROR_OK; 114 | } 115 | 116 | 117 | using BSPReadAsyncCallbackFn = void (*)(BSPError, void*); 118 | 119 | 120 | struct BSPReadAsyncContext { 121 | alignas(0x20) 122 | BSPReadResponse response; 123 | 124 | alignas(0x20) 125 | BSPReadRequest request; 126 | 127 | void* output; 128 | BSPReadAsyncCallbackFn callback; 129 | void* context; 130 | 131 | BSPReadAsyncContext(const char* entity_, 132 | uint32_t instance_, 133 | const char* attribute_, 134 | size_t size_, 135 | void* output_, 136 | BSPReadAsyncCallbackFn callback_, 137 | void* context_) 138 | noexcept: 139 | // intentionally don't initialize response 140 | request{entity_, instance_, attribute_, size_}, 141 | output{output_}, 142 | callback{callback_}, 143 | context{context_} 144 | {} 145 | }; 146 | 147 | 148 | static 149 | void 150 | bspReadAsyncCompleted(IOSError status, void* context) 151 | { 152 | auto read_ctx = reinterpret_cast(context); 153 | BSPError bsp_status = status < 0 ? BSP_ERROR_IOS_ERROR : BSP_ERROR_OK; 154 | if (!bsp_status) 155 | memcpy(read_ctx->output, 156 | read_ctx->response.data, 157 | read_ctx->request.size); 158 | read_ctx->callback(bsp_status, read_ctx->context); 159 | #ifndef AVOID_ALLOCATIONS 160 | delete read_ctx; 161 | #endif 162 | } 163 | 164 | 165 | #ifdef AVOID_ALLOCATIONS 166 | alignas(BSPReadAsyncContext) 167 | char read_async_buf[sizeof(BSPReadAsyncContext)] ; 168 | #endif 169 | 170 | BSPError 171 | bspReadAsync(const char* entity, 172 | uint32_t instance, 173 | const char* attribute, 174 | size_t size, 175 | void* output, 176 | BSPReadAsyncCallbackFn callback, 177 | void* context) 178 | { 179 | if (size > 512) 180 | return BSP_ERROR_SPECIFIED_SIZE_INVALID; 181 | #ifdef AVOID_ALLOCATIONS 182 | auto read_ctx = new(read_async_buf) BSPReadAsyncContext{ 183 | #else 184 | auto read_ctx = new(std::nothrow) BSPReadAsyncContext{ 185 | #endif 186 | entity, instance, attribute, size, 187 | output, 188 | callback, 189 | context 190 | }; 191 | if (!read_ctx) 192 | return BSP_ERROR_IOS_ERROR; 193 | 194 | IOSError status = IOS_IoctlAsync(*bsp_handle_ptr, 195 | BSP_CMD_READ, 196 | &read_ctx->request, 197 | sizeof read_ctx->request, 198 | &read_ctx->response, 199 | size, 200 | bspReadAsyncCompleted, 201 | read_ctx); 202 | if (status < 0) { 203 | #ifndef AVOID_ALLOCATIONS 204 | delete read_ctx; 205 | #endif 206 | OSReport("bspReadAsync(): IOS_IoctlAsync() returned %d\n", status); 207 | return BSP_ERROR_IOS_ERROR; 208 | } 209 | return BSP_ERROR_OK; 210 | } 211 | } // namespace my 212 | 213 | 214 | namespace cpu_mon { 215 | 216 | using get_ppc_utilization_ptr = float (*)(unsigned); 217 | const get_ppc_utilization_ptr get_ppc_utilization = 218 | reinterpret_cast(0x020298d4 - 0xfe3c00); 219 | 220 | #ifdef SYNCHRONOUS_ARM_STAT 221 | 222 | // This is a synchronous call, will reduce frame rate under high IOS activity. 223 | float 224 | get_arm_utilization() 225 | { 226 | float result = 0; 227 | std::uint32_t val = 0; 228 | // Seems to be a value between 0 and 1000. 229 | auto err = my::bspRead("Sys", 0, "cpuUtil", sizeof val, &val); 230 | if (!err) 231 | result = val / 10.0; 232 | 233 | return result; 234 | } 235 | 236 | #else 237 | 238 | alignas(0x20) 239 | std::atomic_uint last_arm_value = 0xffffffff; 240 | 241 | uint32_t arm_result = 0; // used by the async read 242 | 243 | alignas(0x20) 244 | std::atomic_bool read_pending = false; 245 | 246 | 247 | void 248 | got_arm_utilization(BSPError error, void*) 249 | { 250 | if (!error) 251 | last_arm_value.store(arm_result, std::memory_order::relaxed); 252 | else 253 | OSReport("bspReadAsync resulted in error failed\n"); 254 | read_pending.store(false, std::memory_order::release); 255 | } 256 | 257 | 258 | float 259 | get_arm_utilization() 260 | { 261 | if (!read_pending.load(std::memory_order::acquire)) { 262 | // request new async read 263 | read_pending.store(true, std::memory_order::release); 264 | auto err = my::bspReadAsync("Sys", 0, "cpuUtil", 265 | sizeof arm_result, &arm_result, 266 | got_arm_utilization, 267 | nullptr); 268 | if (err) 269 | read_pending.store(false, std::memory_order::release); 270 | } 271 | 272 | float result = 0; 273 | unsigned val = last_arm_value.load(std::memory_order::relaxed); 274 | if (val != 0xffffffff) 275 | result = val / 10.0; // Seems to be a value between 0 and 1000. 276 | 277 | return result; 278 | } 279 | 280 | #endif 281 | 282 | 283 | void 284 | initialize() 285 | {} 286 | 287 | 288 | void 289 | finalize() 290 | {} 291 | 292 | 293 | void 294 | reset() 295 | {} 296 | 297 | 298 | void 299 | get_report(out_span& out, 300 | float) 301 | { 302 | float c0 = get_ppc_utilization(0); 303 | float c1 = get_ppc_utilization(1); 304 | float c2 = get_ppc_utilization(2); 305 | 306 | if (cfg::cpu_busy_percent.value) { 307 | 308 | out.printf("PPC0: %2.1f%% PPC1: %2.1f%% PPC2: %2.1f%%", c0, c1, c2); 309 | if (cfg::cpu_busy_arm.value) { 310 | float c3 = get_arm_utilization(); 311 | out.printf(" ARM: %2.1f%%", c3); 312 | } 313 | 314 | } else { 315 | 316 | using utils::percent_to_bar; 317 | out.printf("CPU: %s %s %s", 318 | percent_to_bar(c0), 319 | percent_to_bar(c1), 320 | percent_to_bar(c2)); 321 | if (cfg::cpu_busy_arm.value) { 322 | float c3 = get_arm_utilization(); 323 | out.printf(" %s", percent_to_bar(c3)); 324 | } 325 | 326 | } 327 | } 328 | 329 | } // namespace cpu_mon 330 | -------------------------------------------------------------------------------- /src/cpu_mon.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef CPU_MON_HPP 10 | #define CPU_MON_HPP 11 | 12 | #include "out_span.hpp" 13 | 14 | 15 | namespace cpu_mon { 16 | 17 | void 18 | initialize(); 19 | 20 | void 21 | finalize(); 22 | 23 | void 24 | reset(); 25 | 26 | void 27 | get_report(out_span& out, 28 | float dt); 29 | 30 | } // namespace cpu_mon 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/fs_mon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * Copyright (C) 2024 Maschell 6 | * 7 | * SPDX-License-Identifier: GPL-3.0-or-later 8 | */ 9 | 10 | /* 11 | * Filesystem Monitoring 12 | * 13 | * While coreinit does have vestigial functions to monitor I/O, they're disabled on retail 14 | * Wii U, and only return error status. 15 | * 16 | * So we do it in a low-level way, by hooking into two internal functions (credits to 17 | * Maschell for these), at the "shim" layer; all higher-level I/O functions are redirected 18 | * to those two functions. 19 | * 20 | * Note that we can't really track I/O that happens under the apps (e.g. kernel, IOSU). If 21 | * you try moving a game between NAND and USB from the system settings, this code can't 22 | * see the I/O happening. 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | 31 | #include 32 | 33 | #include "fs_mon.hpp" 34 | 35 | #include "cfg.hpp" 36 | #include "utils.hpp" 37 | 38 | 39 | namespace fs_mon { 40 | 41 | alignas(0x20) 42 | std::atomic_uint bytes_read = 0; 43 | 44 | alignas(0x20) 45 | std::atomic_uint bytes_written = 0; 46 | 47 | 48 | void 49 | initialize() 50 | { 51 | reset(); 52 | } 53 | 54 | 55 | void 56 | finalize() 57 | {} 58 | 59 | 60 | void 61 | reset() 62 | { 63 | bytes_read = 0; 64 | bytes_written = 0; 65 | } 66 | 67 | 68 | void 69 | get_report(out_span& out, 70 | float dt) 71 | { 72 | using utils::format_bytes; 73 | 74 | if (cfg::fs_perf.value) { 75 | 76 | out.append("fs:"); 77 | 78 | unsigned local_bytes_read = bytes_read.exchange(0u, std::memory_order::relaxed); 79 | float read_rate = local_bytes_read / dt; 80 | 81 | unsigned local_bytes_written = bytes_written.exchange(0u, std::memory_order::relaxed); 82 | float write_rate = local_bytes_written / dt; 83 | 84 | const char* symbol = "\u3000"; // blank space 85 | if (cfg::fs_perf_combined.value) { 86 | if (read_rate > 0 && write_rate > 0) 87 | symbol = "⇅"; 88 | else if (read_rate > 0) 89 | symbol = "↑"; 90 | else if (write_rate > 0) 91 | symbol = "↓"; 92 | 93 | out.append(symbol); 94 | format_bytes(out, read_rate + write_rate); 95 | out.append("/s"); 96 | } else { 97 | out.append("↑"); 98 | format_bytes(out, read_rate); 99 | out.append("/s↓"); 100 | format_bytes(out, write_rate); 101 | out.append("/s"); 102 | } 103 | 104 | } 105 | } 106 | 107 | 108 | // Code below was suggested by Maschell, with some modifications. 109 | 110 | void 111 | update_stats(FSAShimBuffer* shim, 112 | int res) 113 | { 114 | if (res < 0) 115 | return; 116 | 117 | switch (shim->command) { 118 | case FSA_COMMAND_READ_FILE: 119 | bytes_read.fetch_add(shim->request.readFile.size * res, 120 | std::memory_order::relaxed); 121 | break; 122 | case FSA_COMMAND_RAW_READ: 123 | bytes_read.fetch_add(shim->request.rawRead.size * res, 124 | std::memory_order::relaxed); 125 | break; 126 | case FSA_COMMAND_WRITE_FILE: 127 | bytes_written.fetch_add(shim->request.writeFile.size * res, 128 | std::memory_order::relaxed); 129 | break; 130 | case FSA_COMMAND_RAW_WRITE: 131 | bytes_written.fetch_add(shim->request.rawWrite.size * res, 132 | std::memory_order::relaxed); 133 | break; 134 | } 135 | } 136 | 137 | 138 | struct ContextWrapper { 139 | IOSAsyncCallbackFn realCallback; 140 | void* realContext; 141 | FSAShimBuffer* shim; 142 | }; 143 | 144 | 145 | void 146 | async_completed(IOSError result, 147 | void* context) 148 | { 149 | auto wrapper = static_cast(context); 150 | update_stats(wrapper->shim, 151 | __FSAShimDecodeIosErrorToFsaStatus(wrapper->shim->clientHandle, 152 | result)); 153 | if (wrapper->realCallback) 154 | wrapper->realCallback(result, wrapper->realContext); 155 | 156 | delete wrapper; 157 | } 158 | 159 | 160 | DECL_FUNCTION(int, fsaShimSubmitRequest, 161 | FSAShimBuffer* shim, 162 | FSError emulatedError) 163 | { 164 | auto res = real_fsaShimSubmitRequest(shim, emulatedError); 165 | update_stats(shim, res); 166 | return res; 167 | } 168 | 169 | WUPS_MUST_REPLACE_PHYSICAL(fsaShimSubmitRequest, 170 | (0x02042d90 + 0x3001c400), 171 | (0x02042d90 - 0xfe3c00)); 172 | 173 | 174 | DECL_FUNCTION(FSError, fsaShimSubmitRequestAsync, 175 | FSAShimBuffer* shim, 176 | FSError emulatedError, 177 | IOSAsyncCallbackFn callback, 178 | void* context) 179 | { 180 | switch (shim->command) { 181 | case FSA_COMMAND_READ_FILE: 182 | case FSA_COMMAND_RAW_READ: 183 | case FSA_COMMAND_WRITE_FILE: 184 | case FSA_COMMAND_RAW_WRITE: 185 | { 186 | auto wrapper = new(std::nothrow) ContextWrapper{ 187 | .realCallback = callback, 188 | .realContext = context, 189 | .shim = shim 190 | }; 191 | if (!wrapper) 192 | break; // fall back to original callback and context 193 | 194 | auto result = real_fsaShimSubmitRequestAsync(shim, 195 | emulatedError, 196 | async_completed, 197 | wrapper); 198 | if (result != FS_ERROR_OK) { 199 | delete wrapper; 200 | break; // fall back to original callback and context 201 | } 202 | 203 | return result; 204 | } 205 | } 206 | 207 | return real_fsaShimSubmitRequestAsync(shim, 208 | emulatedError, 209 | callback, 210 | context); 211 | } 212 | 213 | WUPS_MUST_REPLACE_PHYSICAL(fsaShimSubmitRequestAsync, 214 | (0x02042e84 + 0x31000000 - 0xfe3c00), 215 | (0x02042e84 - 0x00000000 - 0xfe3c00)); 216 | 217 | } // namespace fs_mon 218 | -------------------------------------------------------------------------------- /src/fs_mon.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef FS_MON_HPP 10 | #define FS_MON_HPP 11 | 12 | #include "out_span.hpp" 13 | 14 | 15 | namespace fs_mon { 16 | 17 | void 18 | initialize(); 19 | 20 | void 21 | finalize(); 22 | 23 | void 24 | reset(); 25 | 26 | void 27 | get_report(out_span& out, 28 | float dt); 29 | 30 | } // namespace fs_mon 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/gx2_mon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * GX2 Monitoring 11 | * 12 | * Currently, `GX2SwapScanBuffers()` is what drives the whole plugin. We count frames, we 13 | * start/stop GPU performance, we ask the overlay to "render", all from this hook. The 14 | * mutual dependency, between this module and the overlay module, is ugly, but 15 | * unavoidable. 16 | * 17 | * We also hook into `GX2Init()` and `GX2Shutdown()`, to ensure we don't call GX2Perf 18 | * functions while GX2 is in an invalid state. 19 | */ 20 | 21 | 22 | #include 23 | #include // malloc(), free() 24 | #include 25 | #include 26 | // #include 27 | #include 28 | #include 29 | 30 | #include // DEBUG 31 | #include 32 | #include 33 | #include // GX2DrawDone() 34 | #include 35 | #include 36 | #include 37 | 38 | #include 39 | 40 | #include 41 | 42 | // WUT lacks 43 | #include "gx2_perf.h" 44 | 45 | #include "gx2_mon.hpp" 46 | 47 | #include "cfg.hpp" 48 | #include "overlay.hpp" 49 | #include "utils.hpp" 50 | 51 | 52 | using std::uint32_t; 53 | 54 | namespace logger = wups::logger; 55 | 56 | 57 | #define TRACE \ 58 | do { \ 59 | auto here = std::source_location::current(); \ 60 | logger::printf("%s:%u: %s\n", \ 61 | here.file_name(), here.line(), \ 62 | here.function_name()); \ 63 | } \ 64 | while (false) 65 | 66 | 67 | /* 68 | * Profiling notes: 69 | * 70 | * `GX2PerfInit()` needs an allocator parameter that's used to allocate/deallocate memory 71 | * during the profiling. To minimize changing memory allocations within the game, we 72 | * allocate memory from libmappedmemory. 73 | */ 74 | 75 | 76 | // Define this to use a Unit Heap instead of an Expanded Heap. 77 | // Unit Heap is faster. 78 | #define USE_UNIT_HEAP 79 | 80 | // Define this to add some error reporting during allocation. 81 | // #define DEBUG_ALLOC_FUNCS 82 | 83 | namespace heap { 84 | 85 | void* raw_memory = nullptr; 86 | const uint32_t raw_size = 4096; 87 | const uint32_t alignment = 32; 88 | 89 | #ifdef USE_UNIT_HEAP 90 | const uint32_t block_size = 32; // Large enough for all GX2Perf allocations. 91 | #endif 92 | 93 | MEMHeapHandle handle = nullptr; 94 | 95 | #ifdef DEBUG_ALLOC_FUNCS 96 | MEMAllocatorAllocFn real_alloc_func = nullptr; 97 | 98 | void* 99 | my_alloc_func(MEMAllocator* a, uint32_t size) 100 | { 101 | #ifdef USE_UNIT_HEAP 102 | if (size > block_size) { 103 | logger::printf("ERROR: trying to allocate %u, but can only allocate up to %u.\n", 104 | size, 105 | block_size); 106 | return nullptr; 107 | } 108 | #endif 109 | 110 | void* result = real_alloc_func(a, size); 111 | logger::printf("allocating %u bytes: %p\n", size, result); 112 | return result; 113 | } 114 | 115 | 116 | MEMAllocatorFreeFn real_free_func = nullptr; 117 | 118 | void 119 | my_free_func(MEMAllocator* a, void* ptr) 120 | { 121 | logger::printf("freeing %p\n", ptr); 122 | return real_free_func(a, ptr); 123 | } 124 | 125 | 126 | MEMAllocatorFunctions my_funcs { 127 | .alloc = my_alloc_func, 128 | .free = my_free_func 129 | }; 130 | #endif 131 | 132 | MEMAllocator 133 | make_allocator() 134 | { 135 | if (!raw_memory) 136 | OSFatal("ERROR!!!!!! Papaya HUD could not allocate memory\n"); 137 | 138 | #ifdef DEBUG_ALLOC_FUNCS 139 | #ifdef USE_UNIT_HEAP 140 | uint32_t total_free = block_size * MEMCountFreeBlockForUnitHeap(handle); 141 | #else 142 | uint32_t total_free = MEMGetTotalFreeSizeForExpHeap(handle); 143 | #endif 144 | logger::printf("Heap total free size: %u\n", total_free); 145 | #endif 146 | 147 | MEMAllocator result; 148 | #ifdef USE_UNIT_HEAP 149 | MEMInitAllocatorForUnitHeap(&result, handle); 150 | #else 151 | MEMInitAllocatorForExpHeap(&result, handle, alignment); 152 | #endif 153 | 154 | #ifdef DEBUG_ALLOC_FUNCS 155 | real_alloc_func = result.funcs->alloc; 156 | real_free_func = result.funcs->free; 157 | result.funcs = &my_funcs; 158 | #endif 159 | 160 | return result; 161 | } 162 | 163 | 164 | void 165 | initialize() 166 | { 167 | if (raw_memory) 168 | return; 169 | 170 | raw_memory = MEMAllocFromMappedMemoryForGX2Ex(raw_size, alignment); 171 | if (raw_memory) { 172 | #ifdef USE_UNIT_HEAP 173 | handle = MEMCreateUnitHeapEx(raw_memory, raw_size, block_size, alignment, 0); 174 | #else 175 | handle = MEMCreateExpHeapEx(lmm_ptr, lmm_size, 0); 176 | #endif 177 | if (!handle) { 178 | MEMFreeToMappedMemory(raw_memory); 179 | raw_memory = nullptr; 180 | } 181 | } 182 | } 183 | 184 | 185 | void 186 | finalize() 187 | { 188 | if (handle) { 189 | #ifdef USE_UNIT_HEAP 190 | MEMDestroyUnitHeap(handle); 191 | #else 192 | MEMDestroyExpHeap(handle); 193 | #endif 194 | handle = nullptr; 195 | } 196 | if (raw_memory) { 197 | MEMFreeToMappedMemory(raw_memory); 198 | raw_memory = nullptr; 199 | } 200 | } 201 | 202 | } // namespace heap 203 | 204 | 205 | template 206 | auto 207 | average(R&& seq) 208 | { 209 | using T = std::ranges::range_value_t; 210 | T sum = T{0}; 211 | unsigned num = 0; 212 | for (const auto& x : seq) { 213 | sum += x; 214 | ++num; 215 | } 216 | return sum / num; 217 | } 218 | 219 | 220 | // TODO: this namespace belongs to a separate module 221 | 222 | namespace gx2 { 223 | 224 | 225 | using metric_or_stat = std::variant; 226 | 227 | 228 | using metric_result = std::variant; 229 | 230 | 231 | metric_result 232 | convert(const GX2MetricResult& res, GX2PerfMetric metric) 233 | { 234 | auto type = GX2GetPerfMetricType(metric); 235 | if (type == GX2_PERF_METRIC_TYPE_U64) 236 | return res.u64Result; 237 | else 238 | return res.f32Result; 239 | } 240 | 241 | 242 | // RAII wrapper for GX2PerfData 243 | struct perf_data { 244 | 245 | GX2PerfData data; 246 | 247 | 248 | perf_data(unsigned max_tags, MEMAllocator& allocator) 249 | { 250 | // logger::printf("checking out allocator\n"); 251 | // logger::printf(" .funcs=%p\n", allocator.funcs); 252 | // logger::printf(" .funcs->alloc=%p\n", allocator.funcs->alloc); 253 | // logger::printf(" .funcs->free=%p\n", allocator.funcs->free); 254 | // logger::printf(" .heap=%p\n", allocator.heap); 255 | // logger::printf(" .arg1=0x%08x\n", allocator.arg1); 256 | // logger::printf(" .arg2=0x%08x\n", allocator.arg2); 257 | 258 | GX2PerfInit(&data, max_tags, &allocator); 259 | 260 | // logger::printf("GX2PerfInit() returned\n"); 261 | } 262 | 263 | 264 | ~perf_data() 265 | { 266 | GX2PerfFree(&data); 267 | } 268 | 269 | 270 | // Delete both copy and move constructors 271 | perf_data(const perf_data&) = delete; 272 | 273 | 274 | // collection method 275 | 276 | void 277 | set_collection_method(GX2PerfCollectionMethod method) 278 | { 279 | GX2PerfSetCollectionMethod(&data, method); 280 | } 281 | 282 | 283 | GX2PerfCollectionMethod 284 | get_collection_method() 285 | const 286 | { 287 | return GX2PerfGetCollectionMethod(&data); 288 | } 289 | 290 | 291 | // metrics 292 | 293 | bool 294 | enable_metric(GX2PerfMetric metric) 295 | { 296 | return GX2PerfMetricEnable(&data, GX2_PERF_TYPE_GPU_METRIC, metric); 297 | } 298 | 299 | 300 | bool 301 | enable_metric(GX2StatId stat) 302 | { 303 | return GX2PerfMetricEnable(&data, GX2_PERF_TYPE_GPU_STAT, stat); 304 | } 305 | 306 | 307 | std::optional 308 | get_metric(uint32_t index) 309 | { 310 | GX2PerfType type; 311 | static_assert(sizeof type == 4); 312 | uint32_t id; 313 | if (!GX2PerfMetricGetEnabled(&data, index, &type, &id)) 314 | return {}; 315 | switch (type) { 316 | case GX2_PERF_TYPE_GPU_METRIC: 317 | return static_cast(id); 318 | case GX2_PERF_TYPE_GPU_STAT: 319 | return static_cast(id); 320 | case GX2_PERF_TYPE_MEM_STAT: // TODO: figure out how to handle this 321 | default: 322 | return {}; 323 | } 324 | } 325 | 326 | 327 | void 328 | clear_metrics() 329 | { 330 | GX2PerfMetricsClear(&data); 331 | } 332 | 333 | 334 | // tags 335 | 336 | void 337 | set_tag(GX2PerfTag tag, bool enable) 338 | { 339 | GX2PerfTagEnable(&data, tag, enable); 340 | } 341 | 342 | 343 | void 344 | enable_all_tags() 345 | { 346 | GX2PerfTagEnableAll(&data); 347 | } 348 | 349 | 350 | bool 351 | is_tag_enabled(GX2PerfTag tag) 352 | const 353 | { 354 | return GX2PerfTagIsEnabled(&data, tag); 355 | } 356 | 357 | 358 | // start/end frame 359 | 360 | void 361 | frame_start() 362 | { 363 | GX2PerfFrameStart(&data); 364 | } 365 | 366 | 367 | void 368 | frame_finish() 369 | { 370 | GX2PerfFrameEnd(&data); 371 | } 372 | 373 | 374 | // start/end pass 375 | 376 | unsigned 377 | get_num_passes() 378 | const 379 | { 380 | return GX2PerfGetNumPasses(&data); 381 | } 382 | 383 | 384 | void 385 | pass_start() 386 | { 387 | GX2PerfPassStart(&data); 388 | } 389 | 390 | 391 | void 392 | pass_finish() 393 | { 394 | GX2PerfPassEnd(&data); 395 | } 396 | 397 | 398 | // start/end tag 399 | 400 | void 401 | tag_start(GX2PerfTag tag) 402 | { 403 | GX2PerfTagStart(&data, tag); 404 | } 405 | 406 | void 407 | tag_finish(GX2PerfTag tag) 408 | { 409 | GX2PerfTagEnd(&data, tag); 410 | } 411 | 412 | 413 | // results 414 | 415 | std::optional 416 | get_frame_result(GX2PerfMetric metric) 417 | const 418 | { 419 | GX2MetricResult result; 420 | if (!GX2PerfGetResultByFrame(&data, 421 | GX2_PERF_TYPE_GPU_METRIC, metric, 422 | &result)) 423 | return {}; 424 | return convert(result, metric); 425 | } 426 | 427 | // Missing: overload get_frame_result() for GX2StatId 428 | 429 | 430 | std::optional 431 | get_tag_result(GX2PerfMetric metric, 432 | unsigned tag, 433 | unsigned number) 434 | const 435 | { 436 | GX2MetricResult result; 437 | 438 | if (!GX2PerfGetResultByTagId(&data, 439 | GX2_PERF_TYPE_GPU_METRIC, metric, 440 | tag, number, 441 | &result)) 442 | return {}; 443 | return convert(result, metric); 444 | } 445 | 446 | // Missing: overload get_tag_result() for GX2StatId 447 | 448 | 449 | // Missing: wrapper for GX2PerfGetResultByTagSequence() 450 | 451 | 452 | // printing 453 | 454 | void 455 | print_frame_results() 456 | const 457 | { 458 | GX2PerfPrintFrameResults(&data); 459 | } 460 | 461 | 462 | // Missing: wrapper for GX2PerfPrintTagResults() 463 | 464 | 465 | // pass coherence 466 | 467 | void 468 | set_pass_coherence(bool enable) 469 | { 470 | GX2PerfSetPassCoherEnable(&data, enable); 471 | } 472 | 473 | 474 | bool 475 | get_pass_coherence() 476 | const 477 | { 478 | return GX2PerfGetPassCoherEnable(&data); 479 | } 480 | 481 | }; 482 | 483 | } // namespace gx2 484 | 485 | 486 | namespace gx2_mon { 487 | 488 | namespace perf { 489 | 490 | struct profiler { 491 | 492 | unsigned pass; 493 | unsigned num_passes; 494 | bool frame_open; 495 | bool pass_open; 496 | bool started; 497 | MEMAllocator allocator; 498 | gx2::perf_data data; 499 | 500 | bool gpu_busy_enabled; 501 | std::vector gpu_busy_vec; 502 | 503 | 504 | profiler() : 505 | pass{0}, 506 | num_passes{0}, 507 | frame_open{false}, 508 | pass_open{false}, 509 | started{false}, 510 | allocator{heap::make_allocator()}, 511 | data{1, allocator}, 512 | gpu_busy_enabled{false} 513 | { 514 | // TRACE; 515 | 516 | data.set_collection_method(GX2_PERF_COLLECT_TAGS_ACCUMULATE); 517 | data.set_tag(0, true); 518 | } 519 | 520 | 521 | ~profiler() 522 | { 523 | // TRACE; 524 | // logger::printf(" frame_open = %s\n", frame_open ? "true" : "false"); 525 | // logger::printf(" started = %s\n", started ? "true" : "false"); 526 | // logger::printf(" pass = %u\n", pass); 527 | // logger::printf(" num_passes = %u\n", num_passes); 528 | } 529 | 530 | 531 | void 532 | start_frame() 533 | { 534 | started = true; 535 | 536 | if (pass == 0) { 537 | // if on frame start, set up all metrics 538 | data.clear_metrics(); 539 | gpu_busy_enabled = data.enable_metric(GX2_PERF_F32_GPU_BUSY); 540 | if (!gpu_busy_enabled) 541 | logger::printf("no slot available for GPU_BUSY\n"); 542 | num_passes = data.get_num_passes(); 543 | data.frame_start(); 544 | frame_open = true; 545 | } 546 | 547 | data.pass_start(); 548 | data.tag_start(0); 549 | pass_open = true; 550 | } 551 | 552 | 553 | void 554 | finish_frame() 555 | { 556 | if (!started) 557 | return; 558 | 559 | if (!frame_open) { 560 | logger::printf("ERROR: frame not open\n"); 561 | return; 562 | } 563 | 564 | if (!pass_open) { 565 | logger::printf("ERROR: pass not open\n"); 566 | return; 567 | } 568 | 569 | data.tag_finish(0); 570 | data.pass_finish(); 571 | pass_open = false; 572 | // GX2DrawDone(); 573 | 574 | // if on last frame 575 | if (++pass >= num_passes) { 576 | data.frame_finish(); 577 | GX2DrawDone(); 578 | if (gpu_busy_enabled) { 579 | auto gpu_busy_res = data.get_frame_result(GX2_PERF_F32_GPU_BUSY); 580 | if (gpu_busy_res) { 581 | float sample = std::get(*gpu_busy_res); 582 | if (gpu_busy_vec.size() < 1000) 583 | gpu_busy_vec.push_back(sample); 584 | else 585 | logger::printf("gpu_busy_vec is growing too much! %u\n", 586 | static_cast(gpu_busy_vec.size())); 587 | } else { 588 | static unsigned error_counter = 0; 589 | ++error_counter; 590 | if (error_counter < 100 || error_counter % 1024 == 0) 591 | logger::printf("failed to get GPU_BUSY result (%u)\n", 592 | error_counter); 593 | } 594 | } 595 | // data.print_frame_results(); 596 | pass = 0; 597 | frame_open = false; 598 | } 599 | } 600 | 601 | }; 602 | 603 | 604 | std::optional prof; 605 | 606 | 607 | void 608 | initialize() 609 | { 610 | if (prof) 611 | return; 612 | 613 | // initialize_lmm_heap(); 614 | 615 | // TRACE; 616 | prof.emplace(); 617 | } 618 | 619 | 620 | void 621 | finalize() 622 | { 623 | if (!prof) 624 | return; 625 | // TRACE; 626 | prof.reset(); 627 | 628 | // finalize_lmm_heap(); 629 | } 630 | 631 | 632 | void 633 | on_frame_start() 634 | { 635 | if (prof) 636 | prof->start_frame(); 637 | } 638 | 639 | 640 | void 641 | on_frame_finish() 642 | { 643 | if (prof) 644 | prof->finish_frame(); 645 | } 646 | 647 | 648 | void 649 | get_report(out_span& out, 650 | float /*dt*/) 651 | { 652 | if (!prof) 653 | return; 654 | 655 | float avg_gpu_busy = average(prof->gpu_busy_vec); 656 | unsigned n_samples = prof->gpu_busy_vec.size(); 657 | prof->gpu_busy_vec.clear(); 658 | 659 | if (n_samples == 0) { 660 | out.append("GPU: ?"); 661 | return; 662 | } 663 | 664 | if (cfg::gpu_busy_percent.value) 665 | out.printf("GPU: %2.1f%%", avg_gpu_busy); 666 | else 667 | out.printf("GPU: %s", utils::percent_to_bar(avg_gpu_busy)); 668 | } 669 | 670 | } // namespace perf 671 | 672 | 673 | namespace fps { 674 | 675 | unsigned counter = 0; 676 | 677 | 678 | void 679 | initialize() 680 | { 681 | counter = 0; 682 | } 683 | 684 | 685 | void 686 | finalize() 687 | {} 688 | 689 | 690 | void 691 | on_frame_start() 692 | {} 693 | 694 | 695 | void 696 | on_frame_finish() 697 | { 698 | ++counter; 699 | } 700 | 701 | 702 | void 703 | get_report(out_span& out, 704 | float dt) 705 | { 706 | float fps = counter / dt; 707 | counter = 0; 708 | out.printf("%02.1f fps", fps); 709 | } 710 | 711 | } // namespace fps 712 | 713 | 714 | namespace resolution { 715 | 716 | struct uvec2 { 717 | unsigned x, y; 718 | 719 | constexpr 720 | bool operator ==(const uvec2& other) 721 | const noexcept = default; 722 | }; 723 | 724 | std::optional tv; 725 | std::optional drc; 726 | 727 | 728 | void 729 | initialize() 730 | { 731 | tv.reset(); 732 | drc.reset(); 733 | } 734 | 735 | 736 | void 737 | finalize() 738 | {} 739 | 740 | 741 | void 742 | on_frame_start() 743 | { 744 | tv.reset(); 745 | drc.reset(); 746 | } 747 | 748 | 749 | void 750 | on_frame_finish() 751 | {} 752 | 753 | 754 | void 755 | get_report(out_span& out, 756 | float) 757 | { 758 | if (tv && drc) { 759 | if (*tv == *drc) // matching resolutions 760 | out.printf("%ux%u", 761 | tv->x, tv->y); 762 | else // different resolutions 763 | out.printf("%ux%u / %ux%u", 764 | tv->x, tv->y, 765 | drc->x, drc->y); 766 | } else { 767 | // only one set 768 | if (tv) 769 | out.printf("%ux%u", tv->x, tv->y); 770 | if (drc) 771 | out.printf("%ux%u", drc->x, drc->y); 772 | } 773 | } 774 | 775 | } // namespace resolution 776 | 777 | 778 | void 779 | initialize() 780 | { 781 | // TRACE; 782 | 783 | // FIFA 13 will call GX2Init() after closing the Home Menu, AFTER it comes into 784 | // the foreground. So we avoid doing any initialization until our GX2Init() hook 785 | // is called. 786 | if (!overlay::gx2_init) 787 | return; 788 | 789 | if (cfg::gpu_busy.value) 790 | perf::initialize(); 791 | 792 | if (cfg::gpu_fps.value) 793 | fps::initialize(); 794 | 795 | if (cfg::gpu_resolution.value) 796 | resolution::initialize(); 797 | } 798 | 799 | 800 | void 801 | finalize() 802 | { 803 | // TRACE; 804 | 805 | perf::finalize(); 806 | fps::finalize(); 807 | } 808 | 809 | 810 | void 811 | reset() 812 | { 813 | // TRACE; 814 | 815 | fps::finalize(); 816 | if (cfg::gpu_fps.value) 817 | fps::initialize(); 818 | 819 | perf::finalize(); 820 | if (cfg::gpu_busy.value) 821 | perf::initialize(); 822 | 823 | } 824 | 825 | 826 | void 827 | on_application_start() 828 | { 829 | heap::initialize(); 830 | } 831 | 832 | 833 | void 834 | on_application_ends() 835 | { 836 | heap::finalize(); 837 | } 838 | 839 | 840 | void 841 | on_frame_start() 842 | { 843 | if (cfg::gpu_busy.value) 844 | perf::on_frame_start(); 845 | 846 | if (cfg::gpu_fps.value) 847 | fps::on_frame_start(); 848 | 849 | if (cfg::gpu_resolution.value) 850 | resolution::on_frame_start(); 851 | } 852 | 853 | 854 | void 855 | on_frame_finish() 856 | { 857 | if (cfg::gpu_busy.value) 858 | perf::on_frame_finish(); 859 | 860 | if (cfg::gpu_fps.value) 861 | fps::on_frame_finish(); 862 | 863 | if (cfg::gpu_resolution.value) 864 | resolution::on_frame_finish(); 865 | } 866 | 867 | 868 | DECL_FUNCTION(void, GX2SwapScanBuffers, 869 | void) 870 | { 871 | overlay::process_toggle_request_from_gx2(); 872 | 873 | // skip all work if the plugin is disabled 874 | if (!cfg::enabled.value) 875 | return real_GX2SwapScanBuffers(); 876 | 877 | on_frame_finish(); 878 | 879 | overlay::render(); 880 | 881 | real_GX2SwapScanBuffers(); 882 | 883 | on_frame_start(); 884 | } 885 | 886 | WUPS_MUST_REPLACE(GX2SwapScanBuffers, 887 | WUPS_LOADER_LIBRARY_GX2, 888 | GX2SwapScanBuffers); 889 | 890 | 891 | DECL_FUNCTION(void, GX2Init, 892 | uint32_t* attr) 893 | { 894 | // logger::printf("GX2Init() was called on core %u\n", OSGetCoreId()); 895 | real_GX2Init(attr); 896 | overlay::gx2_init = true; 897 | 898 | if (cfg::enabled.value) 899 | overlay::create_or_reset(); 900 | } 901 | 902 | WUPS_MUST_REPLACE(GX2Init, 903 | WUPS_LOADER_LIBRARY_GX2, 904 | GX2Init); 905 | 906 | 907 | DECL_FUNCTION(void, GX2Shutdown, 908 | void) 909 | { 910 | // logger::printf("GX2Shutdown() was called\n"); 911 | overlay::destroy(); 912 | overlay::gx2_init = false; 913 | real_GX2Shutdown(); 914 | } 915 | 916 | WUPS_MUST_REPLACE(GX2Shutdown, 917 | WUPS_LOADER_LIBRARY_GX2, 918 | GX2Shutdown); 919 | 920 | 921 | DECL_FUNCTION(void, GX2ResetGPU, 922 | uint32_t arg) 923 | { 924 | // logger::printf("GX2ResetGPU() was called\n"); 925 | overlay::destroy(); 926 | real_GX2ResetGPU(arg); 927 | if (cfg::enabled.value) 928 | overlay::create_or_reset(); 929 | } 930 | 931 | WUPS_MUST_REPLACE(GX2ResetGPU, 932 | WUPS_LOADER_LIBRARY_GX2, 933 | GX2ResetGPU); 934 | 935 | 936 | DECL_FUNCTION(void, GX2CopyColorBufferToScanBuffer, 937 | const GX2ColorBuffer* buffer, 938 | GX2ScanTarget scanTarget) 939 | { 940 | // peek inside the color buffer 941 | if (cfg::enabled.value && cfg::gpu_resolution.value) { 942 | using resolution::uvec2; 943 | switch (scanTarget) { 944 | case GX2_SCAN_TARGET_TV: 945 | resolution::tv = uvec2{buffer->surface.width, buffer->surface.height}; 946 | break; 947 | case GX2_SCAN_TARGET_DRC: 948 | resolution::drc = uvec2{buffer->surface.width, buffer->surface.height}; 949 | break; 950 | default: 951 | ; 952 | } 953 | } 954 | 955 | real_GX2CopyColorBufferToScanBuffer(buffer, scanTarget); 956 | } 957 | 958 | WUPS_MUST_REPLACE(GX2CopyColorBufferToScanBuffer, 959 | WUPS_LOADER_LIBRARY_GX2, 960 | GX2CopyColorBufferToScanBuffer); 961 | 962 | } // namespace gx2_mon 963 | -------------------------------------------------------------------------------- /src/gx2_mon.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef GX2_MON_HPP 10 | #define GX2_MON_HPP 11 | 12 | #include "out_span.hpp" 13 | 14 | 15 | namespace gx2_mon { 16 | 17 | namespace perf { 18 | 19 | void 20 | get_report(out_span& out, 21 | float dt); 22 | 23 | } // namespace perf 24 | 25 | namespace fps { 26 | 27 | void 28 | get_report(out_span&, 29 | float dt); 30 | 31 | } // namespace fps 32 | 33 | namespace resolution { 34 | 35 | void 36 | get_report(out_span&, 37 | float dt); 38 | 39 | } // namespace resolution 40 | 41 | 42 | void 43 | initialize(); 44 | 45 | void 46 | finalize(); 47 | 48 | void 49 | reset(); 50 | 51 | void 52 | on_application_start(); 53 | 54 | void 55 | on_application_ends(); 56 | 57 | } // namespace gx2_mon 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /src/gx2_perf.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | #ifndef GX2_PERF_H 4 | #define GX2_PERF_H 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | 17 | typedef enum GX2PerfCollectionMethod { 18 | GX2_PERF_COLLECT_NONE = 0, 19 | GX2_PERF_COLLECT_TAGS_ACCUMULATE = 1, 20 | GX2_PERF_COLLECT_TAGS_INDIVIDUAL = 2, 21 | } GX2PerfCollectionMethod; 22 | 23 | 24 | typedef enum GX2PerfType { 25 | GX2_PERF_TYPE_GPU_METRIC = 0, 26 | GX2_PERF_TYPE_GPU_STAT = 1, 27 | GX2_PERF_TYPE_MEM_STAT = 2, 28 | } GX2PerfType; 29 | 30 | 31 | typedef enum GX2PerfMetric { 32 | GX2_PERF_U64_TIME = 0x00, 33 | GX2_PERF_U64_GPU_TIME = 0x01, 34 | GX2_PERF_F32_GPU_BUSY = 0x02, 35 | GX2_PERF_F32_SHADER_BUSY = 0x03, 36 | GX2_PERF_U64_REUSED_INDICES_VS = 0x04, 37 | GX2_PERF_F32_SHADER_BUSY_VS = 0x05, 38 | GX2_PERF_F32_SHADER_BUSY_GS = 0x06, 39 | GX2_PERF_F32_SHADER_BUSY_PS = 0x07, 40 | GX2_PERF_F32_ALU_BUSY = 0x08, 41 | GX2_PERF_F32_TEX_BUSY = 0x09, 42 | GX2_PERF_U64_VS_VERTICES_IN = 0x0a, 43 | GX2_PERF_F32_VS_TEX_INST_COUNT = 0x0b, 44 | GX2_PERF_F32_VS_TEX_BUSY = 0x0c, 45 | GX2_PERF_F32_VS_ALU_INST_COUNT = 0x0d, 46 | GX2_PERF_F32_VS_ALU_BUSY = 0x0e, 47 | GX2_PERF_F32_VS_ALU_EFFICIENCY = 0x0f, 48 | GX2_PERF_F32_VS_ALU_TEX_RATIO = 0x10, 49 | GX2_PERF_F32_GS_TEX_INST_COUNT = 0x11, 50 | GX2_PERF_F32_GS_TEX_BUSY = 0x12, 51 | GX2_PERF_F32_GS_ALU_INST_COUNT = 0x13, 52 | GX2_PERF_F32_GS_ALU_BUSY = 0x14, 53 | GX2_PERF_F32_GS_ALU_EFFICIENCY = 0x15, 54 | GX2_PERF_F32_GS_ALU_TEX_RATIO = 0x16, 55 | GX2_PERF_F32_PRIMITIVE_ASSEMBLY_BUSY = 0x17, 56 | GX2_PERF_U64_PRIMITIVES_IN = 0x18, 57 | GX2_PERF_F32_PA_STALLED_ON_RASTERIZER = 0x19, 58 | GX2_PERF_F32_INTERP_BUSY = 0x1a, 59 | GX2_PERF_U64_PS_PIXELS_IN = 0x1b, 60 | GX2_PERF_F32_PS_TEX_INST_COUNT = 0x1c, 61 | GX2_PERF_F32_PS_TEX_BUSY = 0x1d, 62 | GX2_PERF_F32_PS_ALU_INST_COUNT = 0x1e, 63 | GX2_PERF_F32_PS_ALU_BUSY = 0x1f, 64 | GX2_PERF_F32_PS_ALU_EFFICIENCY = 0x20, 65 | GX2_PERF_F32_PS_ALU_TEX_RATIO = 0x21, 66 | GX2_PERF_U64_PS_PIXELS_OUT = 0x22, 67 | GX2_PERF_F32_PS_EXPORT_STALLS = 0x23, 68 | GX2_PERF_F32_TEX_UNIT_BUSY = 0x24, 69 | GX2_PERF_U64_TEXEL_FETCH_COUNT = 0x25, 70 | GX2_PERF_F32_TEX_CACHE_STALLED = 0x26, 71 | GX2_PERF_F32_TEX_MISS_RATE = 0x27, 72 | GX2_PERF_U64_TEX_MEM_BYTES_READ = 0x28, 73 | GX2_PERF_F32_DEPTH_STENCIL_TEST_BUSY = 0x29, 74 | GX2_PERF_F32_HIZ_TRIVIAL_ACCEPT = 0x2a, 75 | GX2_PERF_F32_HIZ_REJECT = 0x2b, 76 | GX2_PERF_U64_PRE_Z_SAMPLES_PASSING = 0x2c, 77 | GX2_PERF_U64_PRE_Z_SAMPLES_FAILING_S = 0x2d, 78 | GX2_PERF_U64_PRE_Z_SAMPLES_FAILING_Z = 0x2e, 79 | GX2_PERF_U64_POST_Z_SAMPLES_PASSING = 0x2f, 80 | GX2_PERF_U64_POST_Z_SAMPLES_FAILING_S = 0x30, 81 | GX2_PERF_U64_POST_Z_SAMPLES_FAILING_Z = 0x31, 82 | GX2_PERF_F32_Z_UNIT_STALLED = 0x32, 83 | GX2_PERF_U64_PIXELS_AT_CB = 0x33, 84 | GX2_PERF_U64_PIXELS_CB_MEM_WRITTEN = 0x34, 85 | GX2_PERF_U64_IA_VERTICES = 0x35, 86 | GX2_PERF_U64_IA_PRIMITIVES = 0x36, 87 | GX2_PERF_U64_VS_INVOCATIONS = 0x37, 88 | GX2_PERF_U64_GS_INVOCATIONS = 0x38, 89 | GX2_PERF_U64_GS_PRIMITIVES = 0x39, 90 | GX2_PERF_U64_C_INVOCATIONS = 0x3a, 91 | GX2_PERF_U64_C_PRIMITIVES = 0x3b, 92 | GX2_PERF_U64_PS_INVOCATIONS = 0x3c, 93 | GX2_PERF_U64_PA_INPUT_PRIM = 0x3d, 94 | } GX2PerfMetric; 95 | 96 | 97 | // TODO: there are a lot of these, this list is incomplete 98 | typedef enum GX2StatId { 99 | GX2_STAT_CP_CP_COUNT = 0x0000, 100 | GX2_STAT_CP_RBIU_FIFO_FULL = 0x0001, 101 | // ... 102 | GX2_STAT_PIPELINE = 0xf000, 103 | } GX2StatId; 104 | 105 | 106 | 107 | typedef uint32_t GX2PerfTag; 108 | 109 | typedef const char* (*GX2PerfTagToStringFunction)(GX2PerfTag tag); 110 | 111 | 112 | typedef union GX2MetricResult { 113 | uint64_t u64Result; 114 | float f32Result; 115 | } GX2MetricResult; 116 | 117 | 118 | #define GX2_PERF_DATA_ALIGNMENT 0x40 119 | 120 | typedef struct WUT_ALIGNAS(GX2_PERF_DATA_ALIGNMENT) GX2PerfData { 121 | uint8_t reserved[0x8a0]; 122 | } GX2PerfData; 123 | 124 | 125 | 126 | typedef enum GX2PerfMetricType { 127 | GX2_PERF_METRIC_TYPE_U64 = 0, 128 | GX2_PERF_METRIC_TYPE_F32 = 1, 129 | } GX2PerfMetricType; 130 | 131 | 132 | GX2PerfMetricType GX2GetPerfMetricType(GX2PerfMetric metric); 133 | 134 | 135 | // init/free 136 | void GX2PerfInit(GX2PerfData* perfData, 137 | uint32_t maxTags, 138 | MEMAllocator* pAllocator); 139 | void GX2PerfFree(GX2PerfData* perfData); 140 | 141 | 142 | // collection method 143 | void GX2PerfSetCollectionMethod(GX2PerfData* perfData, GX2PerfCollectionMethod method); 144 | GX2PerfCollectionMethod GX2PerfGetCollectionMethod(const GX2PerfData* perfData); 145 | 146 | 147 | // metrics 148 | void GX2PerfMetricsClear(GX2PerfData* perfData); 149 | BOOL GX2PerfMetricEnable(GX2PerfData* perfData, 150 | GX2PerfType type, 151 | uint32_t id); 152 | BOOL GX2PerfMetricIsEnabled(const GX2PerfData* perfData, 153 | GX2PerfType type, 154 | uint32_t id); 155 | BOOL GX2PerfMetricGetEnabled(const GX2PerfData* perfData, 156 | uint32_t idx, 157 | GX2PerfType* type, 158 | uint32_t* id); 159 | 160 | 161 | // tags 162 | void GX2PerfTagEnable(GX2PerfData* perfData, 163 | GX2PerfTag tag, 164 | BOOL enable); 165 | void GX2PerfTagEnableAll(GX2PerfData* perfData); 166 | void GX2PerfTagDisableAll(GX2PerfData* perfData); 167 | BOOL GX2PerfTagIsEnabled(const GX2PerfData* perfData, GX2PerfTag tag); 168 | 169 | 170 | // start/end frame 171 | void GX2PerfFrameStart(GX2PerfData* perfData); 172 | void GX2PerfFrameEnd(GX2PerfData* perfData); 173 | 174 | 175 | // start/end tag 176 | uint32_t GX2PerfGetNumPasses(const GX2PerfData* perfData); 177 | void GX2PerfPassStart(GX2PerfData* perfData); 178 | void GX2PerfPassEnd(GX2PerfData* perfData); 179 | 180 | 181 | // start/end tag 182 | void GX2PerfTagStart(GX2PerfData* perfData, GX2PerfTag tag); 183 | void GX2PerfTagEnd(GX2PerfData* perfData, GX2PerfTag tag); 184 | 185 | 186 | // results 187 | BOOL GX2PerfGetResultByFrame(const GX2PerfData* perfData, 188 | GX2PerfType type, 189 | uint32_t id, 190 | GX2MetricResult* result); 191 | BOOL GX2PerfGetResultByTagId(const GX2PerfData* perfData, 192 | GX2PerfType type, 193 | uint32_t id, 194 | uint32_t tag, 195 | uint32_t number, 196 | GX2MetricResult* result); 197 | BOOL GX2PerfGetResultByTagSequence(const GX2PerfData* perfData, 198 | GX2PerfType type, 199 | uint32_t id, 200 | uint32_t sequence, 201 | uint32_t* tag, 202 | uint32_t* number, 203 | uint32_t* depth, 204 | GX2MetricResult* result); 205 | 206 | 207 | // printing 208 | void GX2PerfPrintFrameResults(const GX2PerfData* perfData); 209 | void GX2PerfPrintTagResults(const GX2PerfData* perfData, GX2PerfTagToStringFunction func); 210 | 211 | 212 | // pass coherence 213 | void GX2PerfSetPassCoherEnable(GX2PerfData* perfData, BOOL enable); 214 | BOOL GX2PerfGetPassCoherEnable(const GX2PerfData* perfData); 215 | 216 | 217 | #ifdef __cplusplus 218 | } 219 | #endif 220 | 221 | 222 | #endif 223 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include "cfg.hpp" 17 | #include "gx2_mon.hpp" 18 | #include "overlay.hpp" 19 | #include "time_mon.hpp" 20 | 21 | #ifdef HAVE_CONFIG_H 22 | #include 23 | #endif 24 | 25 | 26 | WUPS_PLUGIN_NAME(PACKAGE_NAME); 27 | WUPS_PLUGIN_VERSION(PACKAGE_VERSION); 28 | WUPS_PLUGIN_DESCRIPTION("Show a HUD."); 29 | WUPS_PLUGIN_AUTHOR("Daniel K. O."); 30 | WUPS_PLUGIN_LICENSE("GPLv3+"); 31 | 32 | WUPS_USE_WUT_DEVOPTAB(); 33 | WUPS_USE_STORAGE(PACKAGE); 34 | 35 | 36 | std::optional app_log_guard; 37 | 38 | 39 | INITIALIZE_PLUGIN() 40 | { 41 | wups::logger::set_prefix(PACKAGE_NAME); 42 | wups::logger::guard log_guard; 43 | 44 | try { 45 | wups::shortcut::initialize(PACKAGE_NAME); 46 | 47 | cfg::initialize(); 48 | overlay::initialize(); 49 | } 50 | catch (std::exception& e) { 51 | wups::logger::printf("init error: %s\n", e.what()); 52 | } 53 | } 54 | 55 | 56 | DEINITIALIZE_PLUGIN() 57 | { 58 | wups::logger::guard log_guard; 59 | 60 | cfg::finalize(); 61 | overlay::finalize(); 62 | 63 | wups::shortcut::finalize(); 64 | } 65 | 66 | 67 | ON_APPLICATION_START() 68 | { 69 | app_log_guard.emplace(); 70 | gx2_mon::on_application_start(); 71 | time_mon::on_application_start(); 72 | } 73 | 74 | 75 | ON_APPLICATION_REQUESTS_EXIT() 76 | { 77 | overlay::destroy(); 78 | } 79 | 80 | 81 | ON_APPLICATION_ENDS() 82 | { 83 | gx2_mon::on_application_ends(); 84 | app_log_guard.reset(); 85 | } 86 | 87 | 88 | ON_ACQUIRED_FOREGROUND() 89 | { 90 | overlay::on_acquired_foreground(); 91 | } 92 | 93 | 94 | ON_RELEASE_FOREGROUND() 95 | { 96 | overlay::on_release_foreground(); 97 | } 98 | -------------------------------------------------------------------------------- /src/net_mon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * Network Monitoring 11 | * 12 | * One of the simplest modules, we just hook into send and recv functions from nsysnet. 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include // struct sockaddr 21 | #include 22 | 23 | #include "net_mon.hpp" 24 | 25 | #include "cfg.hpp" 26 | #include "utils.hpp" 27 | 28 | 29 | namespace net_mon { 30 | 31 | alignas(0x20) 32 | std::atomic_uint bytes_received = 0; 33 | 34 | alignas(0x20) 35 | std::atomic_uint bytes_sent = 0; 36 | 37 | 38 | void 39 | initialize() 40 | { 41 | reset(); 42 | } 43 | 44 | 45 | void 46 | finalize() 47 | {} 48 | 49 | 50 | void 51 | reset() 52 | { 53 | bytes_received = 0; 54 | bytes_sent = 0; 55 | } 56 | 57 | 58 | void 59 | get_report(out_span& out, 60 | float dt) 61 | { 62 | const char* separator = ""; 63 | 64 | out.append("net: "); 65 | 66 | if (cfg::net_cfg.value) { 67 | int res; 68 | NetConfCfg cfg{}; 69 | res = netconf_init(); 70 | if (!res) { 71 | res = netconf_get_running(&cfg); 72 | if (!res) { 73 | if (cfg.wl0.if_state) { 74 | out.append("wifi \""); 75 | out.append(cfg.wifi.config.ssid, 76 | cfg.wifi.config.ssidlength); 77 | out.append("\""); 78 | } else if (cfg.eth0.if_state) 79 | out.append("eth"); 80 | else // unlikely scenario when neither wl0 nor eth0 are enabled 81 | out.append("none"); 82 | } else { 83 | // when netconf_get_running() fails 84 | out.append("none"); 85 | } 86 | netconf_close(); 87 | } else { 88 | // when netconf_init() fails 89 | out.append("none"); 90 | } 91 | separator = " "; 92 | } 93 | 94 | if (cfg::net_bw.value) { 95 | using utils::format_bytes; 96 | 97 | unsigned local_bytes_received = bytes_received.exchange(0u, 98 | std::memory_order::relaxed); 99 | float down_rate = local_bytes_received / dt; 100 | 101 | unsigned local_bytes_sent = bytes_sent.exchange(0u, 102 | std::memory_order::relaxed); 103 | float up_rate = local_bytes_sent / dt; 104 | 105 | const char* symbol = "\u3000"; // blank space 106 | 107 | if (cfg::net_bw_combined.value) { 108 | if (down_rate > 0 && up_rate > 0) 109 | symbol = "⇅"; 110 | else if (down_rate > 0) 111 | symbol = "↓"; 112 | else if (up_rate > 0) 113 | symbol = "↑"; 114 | 115 | out.printf("%s%s", separator, symbol); 116 | format_bytes(out, down_rate + up_rate); 117 | out.append("/s"); 118 | } else { 119 | out.append(separator); 120 | out.append("↓"); 121 | format_bytes(out, down_rate); 122 | out.append("/s↑"); 123 | format_bytes(out, up_rate); 124 | out.append("/s"); 125 | } 126 | } 127 | 128 | } 129 | 130 | 131 | DECL_FUNCTION(int, recv, 132 | int fd, 133 | void* buf, 134 | int len, 135 | int flags) 136 | { 137 | int result = real_recv(fd, buf, len, flags); 138 | if (result != -1 && cfg::net_bw.value) 139 | bytes_received.fetch_add(result, std::memory_order::relaxed); 140 | return result; 141 | } 142 | 143 | 144 | DECL_FUNCTION(int, recvfrom, 145 | int fd, 146 | void* buf, 147 | int len, 148 | int flags, 149 | struct sockaddr* src, 150 | int* src_len) 151 | { 152 | int result = real_recvfrom(fd, buf, len, flags, src, src_len); 153 | if (result != -1 && cfg::net_bw.value) 154 | bytes_received.fetch_add(result, std::memory_order::relaxed); 155 | return result; 156 | } 157 | 158 | 159 | DECL_FUNCTION(int, recvfrom_ex, 160 | int fd, 161 | void* buf, 162 | int len, 163 | int flags, 164 | struct sockaddr* src, 165 | int* src_len, 166 | void* msg, 167 | int msg_len) 168 | { 169 | int result = real_recvfrom_ex(fd, buf, len, flags, src, src_len, msg, msg_len); 170 | if (result != -1 && cfg::net_bw.value) 171 | bytes_received.fetch_add(result, std::memory_order::relaxed); 172 | return result; 173 | } 174 | 175 | 176 | DECL_FUNCTION(int, recvfrom_multi, 177 | int fd, 178 | int flags, 179 | void* buffs, 180 | int data_len, 181 | int data_count, 182 | struct timeval* timeout) 183 | { 184 | int result = real_recvfrom_multi(fd, flags, buffs, data_len, data_count, timeout); 185 | if (result != -1 && cfg::net_bw.value) 186 | bytes_received.fetch_add(result, std::memory_order::relaxed); 187 | return result; 188 | } 189 | 190 | 191 | DECL_FUNCTION(int, send, 192 | int fd, 193 | const void* buf, 194 | int len, 195 | int flags) 196 | { 197 | int result = real_send(fd, buf, len, flags); 198 | if (result != -1 && cfg::net_bw.value) 199 | bytes_sent.fetch_add(result, std::memory_order::relaxed); 200 | return result; 201 | } 202 | 203 | 204 | DECL_FUNCTION(int, sendto, 205 | int fd, 206 | const void* buf, 207 | int len, 208 | int flags, 209 | const struct sockaddr* dst, 210 | int dst_len) 211 | { 212 | int result = real_sendto(fd, buf, len, flags, dst, dst_len); 213 | if (result != -1 && cfg::net_bw.value) 214 | bytes_sent.fetch_add(result, std::memory_order::relaxed); 215 | return result; 216 | } 217 | 218 | 219 | DECL_FUNCTION(int, sendto_multi, 220 | int fd, 221 | const void *buf, 222 | int len, 223 | int flags, 224 | const struct sockaddr* dstv, 225 | int dstv_len) 226 | { 227 | int result = real_sendto_multi(fd, buf, len, flags, dstv, dstv_len); 228 | if (result != -1 && cfg::net_bw.value) 229 | bytes_sent.fetch_add(result, std::memory_order::relaxed); 230 | return result; 231 | } 232 | 233 | 234 | DECL_FUNCTION(int, sendto_multi_ex, 235 | int fd, 236 | int flags, 237 | void* buffs, 238 | int count) 239 | { 240 | int result = real_sendto_multi_ex(fd, flags, buffs, count); 241 | if (result != -1 && cfg::net_bw.value) 242 | bytes_sent.fetch_add(result, std::memory_order::relaxed); 243 | return result; 244 | } 245 | 246 | 247 | WUPS_MUST_REPLACE(recv, WUPS_LOADER_LIBRARY_NSYSNET, recv); 248 | WUPS_MUST_REPLACE(recvfrom, WUPS_LOADER_LIBRARY_NSYSNET, recvfrom); 249 | WUPS_MUST_REPLACE(recvfrom_ex, WUPS_LOADER_LIBRARY_NSYSNET, recvfrom_ex); 250 | WUPS_MUST_REPLACE(recvfrom_multi, WUPS_LOADER_LIBRARY_NSYSNET, recvfrom_multi); 251 | 252 | WUPS_MUST_REPLACE(send, WUPS_LOADER_LIBRARY_NSYSNET, send); 253 | WUPS_MUST_REPLACE(sendto, WUPS_LOADER_LIBRARY_NSYSNET, sendto); 254 | WUPS_MUST_REPLACE(sendto_multi, WUPS_LOADER_LIBRARY_NSYSNET, sendto_multi); 255 | WUPS_MUST_REPLACE(sendto_multi_ex, WUPS_LOADER_LIBRARY_NSYSNET, sendto_multi_ex); 256 | 257 | } // namespace net_mon 258 | -------------------------------------------------------------------------------- /src/net_mon.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef NET_MON_HPP 10 | #define NET_MON_HPP 11 | 12 | #include "out_span.hpp" 13 | 14 | 15 | namespace net_mon { 16 | 17 | void 18 | initialize(); 19 | 20 | void 21 | finalize(); 22 | 23 | void 24 | reset(); 25 | 26 | void 27 | get_report(out_span& out, 28 | float dt); 29 | 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/out_span.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "out_span.hpp" 14 | 15 | 16 | using std::size_t; 17 | 18 | 19 | out_span::out_span(char* buf, size_t buf_size) 20 | noexcept : 21 | data{buf}, 22 | size{buf_size} 23 | {} 24 | 25 | 26 | bool 27 | out_span::eof() 28 | const noexcept 29 | { 30 | return size <= 1; 31 | } 32 | 33 | 34 | void 35 | out_span::advance(size_t amount) 36 | noexcept 37 | { 38 | if (amount > size) 39 | amount = size; 40 | size -= amount; 41 | data += amount; 42 | } 43 | 44 | 45 | void 46 | out_span::append(const char* str) 47 | noexcept 48 | { 49 | if (eof()) 50 | return; 51 | 52 | while (size > 1 && *str) { 53 | *data++ = *str++; 54 | --size; 55 | } 56 | if (size > 0) 57 | *data = '\0'; 58 | } 59 | 60 | 61 | void 62 | out_span::append(const char* str, 63 | size_t n) 64 | noexcept 65 | { 66 | if (eof()) 67 | return; 68 | 69 | while (size > 1 && *str && n--) { 70 | *data++ = *str++; 71 | --size; 72 | } 73 | if (size > 0) 74 | *data = '\0'; 75 | } 76 | 77 | 78 | void 79 | out_span::printf(const char* fmt, 80 | ...) 81 | noexcept 82 | { 83 | if (eof()) 84 | return; 85 | va_list args; 86 | va_start(args, fmt); 87 | int written = std::vsnprintf(data, size, fmt, args); 88 | va_end(args); 89 | if (written <= 0) 90 | return; 91 | advance(written); 92 | } 93 | -------------------------------------------------------------------------------- /src/out_span.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef OUT_SPAN_HPP 10 | #define OUT_SPAN_HPP 11 | 12 | #include 13 | 14 | 15 | struct out_span { 16 | 17 | char* data; 18 | std::size_t size; 19 | 20 | out_span(char* buf, std::size_t buf_size) 21 | noexcept; 22 | 23 | bool 24 | eof() 25 | const noexcept; 26 | 27 | void 28 | advance(std::size_t amount) 29 | noexcept; 30 | 31 | void 32 | append(const char* str) 33 | noexcept; 34 | 35 | void 36 | append(const char* str, 37 | std::size_t n) 38 | noexcept; 39 | 40 | __attribute__(( __format__ ( __printf__, 2, 3 ) )) 41 | void 42 | printf(const char* fmt, 43 | ...) 44 | noexcept; 45 | 46 | }; 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/overlay.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * This is the "overlay" that shows the HUD. 11 | * 12 | * Currently we just (ab)use the Notification module, showing a persistent dynamic 13 | * notification. At every update interval, we update the notification text. We don't have 14 | * to write any code to actually render things, but we pay a hefty price: updating a 15 | * notification requires locking one or more mutexes in the Notification module; not a 16 | * good thing to have hooked into `GX2SwapScanBuffers()`. 17 | * 18 | * This will be later replaced by an actual overlay rendering implementation, requiring no 19 | * mutexes. Then we can have more advanced rendering, like line graphs and histograms. 20 | */ 21 | 22 | #include 23 | 24 | #include 25 | #include 26 | 27 | #include 28 | 29 | #include 30 | #include 31 | 32 | #include "overlay.hpp" 33 | 34 | #include "ax_mon.hpp" 35 | #include "cfg.hpp" 36 | #include "cpu_mon.hpp" 37 | #include "fs_mon.hpp" 38 | #include "gx2_mon.hpp" 39 | #include "net_mon.hpp" 40 | #include "pad_mon.hpp" 41 | #include "time_mon.hpp" 42 | #include "utils.hpp" 43 | 44 | 45 | // #define TEST_TIME 46 | 47 | 48 | namespace overlay { 49 | 50 | namespace logger = wups::logger; 51 | 52 | 53 | bool gx2_init = false; 54 | std::atomic_bool toggle_requested = false; 55 | 56 | std::atomic notif_handle{0}; 57 | 58 | OSTime last_sample_time; 59 | 60 | 61 | namespace { 62 | 63 | void 64 | on_notif_finished(NotificationModuleHandle h, void*) 65 | { 66 | if (h == notif_handle.load()) 67 | notif_handle.store(0); 68 | } 69 | 70 | 71 | NMColor 72 | convert(wups::color c) 73 | { 74 | return {c.r, c.g, c.b, c.a}; 75 | } 76 | 77 | } 78 | 79 | 80 | void 81 | initialize() 82 | { 83 | NotificationModule_InitLibrary(); 84 | } 85 | 86 | 87 | void 88 | finalize() 89 | { 90 | destroy(); 91 | NotificationModule_DeInitLibrary(); 92 | } 93 | 94 | 95 | void 96 | create_or_reset() 97 | { 98 | // Don't create anything until GX2Init() is called. 99 | if (!gx2_init) 100 | return; 101 | 102 | auto handle = notif_handle.load(); 103 | if (!handle) { 104 | auto status = NotificationModule_AddDynamicNotificationEx(CAFE_GLYPH_HELP, 105 | &handle, 106 | convert(cfg::color_fg.value), 107 | convert(cfg::color_bg.value), 108 | on_notif_finished, 109 | nullptr, 110 | false); 111 | if (status != NOTIFICATION_MODULE_RESULT_SUCCESS) { 112 | logger::printf("Failed to create overlay notification: %s\n", 113 | NotificationModule_GetStatusStr(status)); 114 | return; 115 | } 116 | notif_handle.store(handle); 117 | } 118 | 119 | reset(); 120 | } 121 | 122 | 123 | void 124 | destroy() 125 | { 126 | ax_mon::finalize(); 127 | time_mon::finalize(); 128 | gx2_mon::finalize(); 129 | cpu_mon::finalize(); 130 | net_mon::finalize(); 131 | fs_mon::finalize(); 132 | pad_mon::finalize(); 133 | 134 | auto handle = notif_handle.load(); 135 | if (!handle) 136 | return; 137 | 138 | auto status = NotificationModule_FinishDynamicNotification(handle, 0); 139 | if (status != NOTIFICATION_MODULE_RESULT_SUCCESS) { 140 | logger::printf("Failed to finish notification: %s\n", 141 | NotificationModule_GetStatusStr(status)); 142 | return; 143 | } 144 | } 145 | 146 | 147 | void 148 | reset() 149 | { 150 | last_sample_time = OSGetSystemTime(); 151 | 152 | auto handle = notif_handle.load(); 153 | if (handle) { 154 | NotificationModule_UpdateDynamicNotificationTextColor(handle, 155 | convert(cfg::color_fg.value)); 156 | NotificationModule_UpdateDynamicNotificationBackgroundColor(handle, 157 | convert(cfg::color_bg.value)); 158 | } 159 | 160 | ax_mon::reset(); 161 | time_mon::reset(); 162 | gx2_mon::reset(); 163 | cpu_mon::reset(); 164 | net_mon::reset(); 165 | fs_mon::reset(); 166 | pad_mon::reset(); 167 | 168 | } 169 | 170 | 171 | void 172 | on_acquired_foreground() 173 | { 174 | if (cfg::enabled.value) 175 | create_or_reset(); 176 | } 177 | 178 | 179 | void 180 | on_release_foreground() 181 | { 182 | ax_mon::finalize(); 183 | time_mon::finalize(); 184 | gx2_mon::finalize(); 185 | cpu_mon::finalize(); 186 | net_mon::finalize(); 187 | fs_mon::finalize(); 188 | pad_mon::finalize(); 189 | } 190 | 191 | 192 | void 193 | render() 194 | { 195 | auto handle = notif_handle.load(); 196 | if (!handle) 197 | return; 198 | 199 | const OSTime update_interval = OSMillisecondsToTicks(cfg::interval.value.count()); 200 | 201 | OSTime now = OSGetSystemTime(); 202 | // If it's time to update the notification 203 | if (now - last_sample_time >= update_interval) { 204 | try { 205 | static char buffer[256]; 206 | out_span output{buffer, sizeof buffer}; 207 | buffer[0] = '\0'; 208 | 209 | const char* separator = ""; 210 | 211 | auto append_separator = [&output, &separator] 212 | { 213 | output.append(separator); 214 | separator = utils::field_separator; 215 | }; 216 | 217 | const float dt = (now - last_sample_time) / float(OSTimerClockSpeed); 218 | 219 | if (cfg::time.value || cfg::uptime.value || cfg::play_time.value) { 220 | append_separator(); 221 | time_mon::get_report(output, dt); 222 | } 223 | 224 | if (cfg::gpu_fps.value) { 225 | append_separator(); 226 | gx2_mon::fps::get_report(output, dt); 227 | } 228 | 229 | if (cfg::gpu_resolution.value) { 230 | append_separator(); 231 | gx2_mon::resolution::get_report(output, dt); 232 | } 233 | 234 | if (cfg::gpu_busy.value) { 235 | append_separator(); 236 | gx2_mon::perf::get_report(output, dt); 237 | } 238 | 239 | if (cfg::cpu_busy.value) { 240 | append_separator(); 241 | cpu_mon::get_report(output, dt); 242 | } 243 | 244 | if (cfg::net_bw.value || cfg::net_cfg.value) { 245 | append_separator(); 246 | net_mon::get_report(output, dt); 247 | } 248 | 249 | if (cfg::fs_perf.value) { 250 | append_separator(); 251 | fs_mon::get_report(output, dt); 252 | } 253 | 254 | if (cfg::audio.value) { 255 | append_separator(); 256 | ax_mon::get_report(output, dt); 257 | } 258 | 259 | if (cfg::button_rate.value || cfg::battery.value) { 260 | append_separator(); 261 | pad_mon::get_report(output, dt); 262 | } 263 | 264 | // WORKAROUND: NotificationsModule doesn't like empty text. 265 | if (buffer[0] == '\0') 266 | output.append(CAFE_GLYPH_HELP); 267 | 268 | NotificationModule_UpdateDynamicNotificationText(handle, buffer); 269 | 270 | last_sample_time = now; 271 | 272 | #ifdef TEST_TIME 273 | // check how long it takes to calculate all fields 274 | now = OSGetSystemTime(); 275 | OSTime delta = now - last_sample_time; 276 | logger::printf("Overlay render time = %lld (%f us)\n", 277 | delta, 278 | (double)OSTicksToMicroseconds(delta)); 279 | #endif 280 | } 281 | catch (std::exception& e) { 282 | logger::printf("Error in overlay::render(): %s\n", e.what()); 283 | } 284 | 285 | } // if it's time to update 286 | } 287 | 288 | 289 | void 290 | toggle() 291 | { 292 | toggle_requested = true; 293 | OSMemoryBarrier(); 294 | } 295 | 296 | 297 | void 298 | process_toggle_request_from_gx2() 299 | { 300 | if (!toggle_requested) [[likely]] 301 | return; 302 | toggle_requested = false; 303 | cfg::enabled.value = !cfg::enabled.value; 304 | /* 305 | * Note: avoid saving when toggling through the shortcut.\ 306 | * 307 | * It's known to crash when a big SDCafiine mod pack is being used, and the config 308 | * is save while there's I/O happening. 309 | */ 310 | // cfg::save(); 311 | if (cfg::enabled.value) 312 | overlay::create_or_reset(); 313 | else 314 | overlay::destroy(); 315 | } 316 | 317 | } // namespace overlay 318 | -------------------------------------------------------------------------------- /src/overlay.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef OVERLAY_HPP 10 | #define OVERLAY_HPP 11 | 12 | 13 | namespace overlay { 14 | 15 | extern bool gx2_init; 16 | 17 | 18 | void initialize(); 19 | void finalize(); 20 | 21 | void create_or_reset(); 22 | void destroy(); 23 | void reset(); 24 | 25 | 26 | void on_acquired_foreground(); 27 | 28 | void on_release_foreground(); 29 | 30 | 31 | void render(); 32 | 33 | 34 | void toggle(); 35 | void process_toggle_request_from_gx2(); 36 | 37 | } // namespace overlay 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /src/pad_mon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * Gamepad/Wiimote Monitoring 11 | */ 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | 30 | #include "pad_mon.hpp" 31 | 32 | #include "cfg.hpp" 33 | #include "overlay.hpp" 34 | #include "utils.hpp" 35 | 36 | 37 | using std::array; 38 | using std::int32_t; 39 | using std::uint32_t; 40 | using std::uint16_t; 41 | 42 | 43 | namespace logger = wups::logger; 44 | 45 | 46 | namespace pad_mon { 47 | 48 | enum VPADBatteryLevel : unsigned { 49 | VPAD_BATTERY_CHARGING, 50 | VPAD_BATTERY_LEVEL_EMPTY, 51 | VPAD_BATTERY_LEVEL_0, // no bars 52 | VPAD_BATTERY_LEVEL_1, 53 | VPAD_BATTERY_LEVEL_2, 54 | VPAD_BATTERY_LEVEL_3, 55 | VPAD_BATTERY_LEVEL_4, // max bars 56 | }; 57 | 58 | 59 | enum WPADBatteryLevel : unsigned { 60 | WPAD_BATTERY_LEVEL_0, // no bars 61 | WPAD_BATTERY_LEVEL_1, 62 | WPAD_BATTERY_LEVEL_2, 63 | WPAD_BATTERY_LEVEL_3, 64 | WPAD_BATTERY_LEVEL_4, // max bars 65 | 66 | // hack to easily handle Pro Controller 67 | PRO_BATTERY_CHARGING = 0x100, 68 | PRO_BATTERY_WIRED = 0x200, 69 | }; 70 | 71 | 72 | const char* 73 | vpad_charge_to_bar(unsigned level) 74 | noexcept 75 | { 76 | switch (level) { 77 | case VPAD_BATTERY_CHARGING: 78 | return "C"; 79 | case VPAD_BATTERY_LEVEL_EMPTY: 80 | return "X"; 81 | case VPAD_BATTERY_LEVEL_0: 82 | return "\u3000"; 83 | case VPAD_BATTERY_LEVEL_1: 84 | return "▂"; 85 | case VPAD_BATTERY_LEVEL_2: 86 | return "▄"; 87 | case VPAD_BATTERY_LEVEL_3: 88 | return "▆"; 89 | case VPAD_BATTERY_LEVEL_4: 90 | return "█"; 91 | default: 92 | return "?"; 93 | } 94 | } 95 | 96 | 97 | const char* 98 | vpad_charge_to_percent(unsigned level) 99 | noexcept 100 | { 101 | switch (level) { 102 | case VPAD_BATTERY_CHARGING: 103 | return "C"; 104 | case VPAD_BATTERY_LEVEL_EMPTY: 105 | return "X"; 106 | case VPAD_BATTERY_LEVEL_0: 107 | return "0%"; 108 | case VPAD_BATTERY_LEVEL_1: 109 | return "25%"; 110 | case VPAD_BATTERY_LEVEL_2: 111 | return "50%"; 112 | case VPAD_BATTERY_LEVEL_3: 113 | return "75%"; 114 | case VPAD_BATTERY_LEVEL_4: 115 | return "100%"; 116 | default: 117 | return "?"; 118 | } 119 | } 120 | 121 | 122 | const char* 123 | wpad_charge_to_bar(unsigned level) 124 | noexcept 125 | { 126 | switch (level) { 127 | case WPAD_BATTERY_LEVEL_0: 128 | return "\u3000"; 129 | case WPAD_BATTERY_LEVEL_1: 130 | return "▂"; 131 | case WPAD_BATTERY_LEVEL_2: 132 | return "▄"; 133 | case WPAD_BATTERY_LEVEL_3: 134 | return "▆"; 135 | case WPAD_BATTERY_LEVEL_4: 136 | return "█"; 137 | default: 138 | if (level & PRO_BATTERY_CHARGING) 139 | return "C"; 140 | if (level & PRO_BATTERY_WIRED) 141 | return "W"; 142 | return "?"; 143 | } 144 | } 145 | 146 | 147 | const char* 148 | wpad_charge_to_percent(unsigned level) 149 | noexcept 150 | { 151 | switch (level) { 152 | case WPAD_BATTERY_LEVEL_0: 153 | return "0%"; 154 | case WPAD_BATTERY_LEVEL_1: 155 | return "25%"; 156 | case WPAD_BATTERY_LEVEL_2: 157 | return "50%"; 158 | case WPAD_BATTERY_LEVEL_3: 159 | return "75%"; 160 | case WPAD_BATTERY_LEVEL_4: 161 | return "100%"; 162 | default: 163 | if (level & PRO_BATTERY_CHARGING) 164 | return "C"; 165 | if (level & PRO_BATTERY_WIRED) 166 | return "W"; 167 | return "?"; 168 | } 169 | } 170 | 171 | 172 | std::array pro_power; 173 | 174 | 175 | alignas(0x20) 176 | std::atomic_uint button_presses = 0; 177 | 178 | struct alignas(0x20) vpad_state_t { 179 | std::atomic_uint battery = 0; 180 | std::atomic_bool attached = false; 181 | 182 | void 183 | reset() 184 | noexcept 185 | { 186 | battery = 0; 187 | attached = false; 188 | } 189 | }; 190 | 191 | std::array vpad_states; 192 | 193 | 194 | // Simple class to track buttons triggered and released. 195 | template 196 | struct button_tracker { 197 | 198 | T hold = 0; 199 | T trigger = 0; 200 | T release = 0; 201 | 202 | 203 | void 204 | reset() 205 | noexcept 206 | { 207 | hold = 0; 208 | trigger = 0; 209 | release = 0; 210 | } 211 | 212 | 213 | void 214 | update(T buttons) 215 | noexcept 216 | { 217 | T changed = hold ^ buttons; 218 | hold = buttons; 219 | trigger = changed & buttons; 220 | release = changed & ~buttons; 221 | } 222 | 223 | }; 224 | 225 | 226 | struct alignas(0x20) wpad_state_t { 227 | button_tracker core; 228 | button_tracker ext; 229 | uint8_t ext_type = WPAD_EXT_CORE; 230 | 231 | void 232 | reset() 233 | noexcept 234 | { 235 | core.reset(); 236 | ext.reset(); 237 | } 238 | 239 | void 240 | update_ext_type(uint8_t et) 241 | noexcept 242 | { 243 | if (ext_type != et) { 244 | ext.reset(); 245 | ext_type = et; 246 | } 247 | } 248 | }; 249 | 250 | 251 | std::array wpad_states; 252 | 253 | 254 | void 255 | initialize() 256 | { 257 | reset(); 258 | } 259 | 260 | 261 | void 262 | finalize() 263 | {} 264 | 265 | 266 | void 267 | reset() 268 | { 269 | button_presses = 0; 270 | for (auto& vpad : vpad_states) 271 | vpad.reset(); 272 | for (auto& wpad : wpad_states) 273 | wpad.reset(); 274 | } 275 | 276 | 277 | void 278 | get_report(out_span& out, 279 | float dt) 280 | { 281 | const char* separator = ""; 282 | 283 | if (cfg::button_rate.value) { 284 | unsigned presses = std::atomic_exchange(&button_presses, 0u); 285 | float presses_rate = presses / dt; 286 | out.printf("%.1f bps", presses_rate); 287 | separator = utils::field_separator; 288 | } 289 | 290 | if (cfg::battery.value) { 291 | 292 | out.append(separator); 293 | out.append("bat:"); 294 | 295 | const char* icon = CAFE_GLYPH_GAMEPAD; 296 | 297 | separator = " "; 298 | for (unsigned i = 0; i < 2; ++i) { 299 | if (vpad_states[i].attached.load(std::memory_order::acquire)) { 300 | unsigned battery = vpad_states[i].battery.load(std::memory_order::relaxed); 301 | out.append(separator); 302 | out.append(icon); 303 | if (cfg::battery_percent.value) 304 | out.append(vpad_charge_to_percent(battery)); 305 | else 306 | out.append(vpad_charge_to_bar(battery)); 307 | // logger::printf("vpad%u: %u\n", i, battery); 308 | } 309 | } 310 | 311 | icon = CAFE_GLYPH_WIIMOTE; 312 | for (unsigned i = 0; i < 7; ++i) { 313 | auto channel = static_cast(i); 314 | WPADExtensionType ext; 315 | if (WPADProbe(channel, &ext)) 316 | continue; 317 | 318 | unsigned battery = WPADGetBatteryLevel(channel); 319 | 320 | if (ext == WPAD_EXT_PRO_CONTROLLER) 321 | if (pro_power[channel]) 322 | battery = pro_power[channel]; 323 | 324 | out.append(separator); 325 | out.append(icon); 326 | if (cfg::battery_percent.value) 327 | out.append(wpad_charge_to_percent(battery)); 328 | else 329 | out.append(wpad_charge_to_bar(battery)); 330 | // logger::printf("wpad%u: %u\n", i, battery); 331 | } 332 | 333 | } 334 | } 335 | 336 | 337 | // Note: we use this mask to ignore emulated buttons 338 | constexpr uint32_t vpad_mask = 339 | VPAD_BUTTON_UP | VPAD_BUTTON_DOWN | 340 | VPAD_BUTTON_LEFT | VPAD_BUTTON_RIGHT | 341 | VPAD_BUTTON_L | VPAD_BUTTON_R | 342 | VPAD_BUTTON_ZL | VPAD_BUTTON_ZR | 343 | VPAD_BUTTON_A | VPAD_BUTTON_B | 344 | VPAD_BUTTON_X | VPAD_BUTTON_Y | 345 | VPAD_BUTTON_PLUS | VPAD_BUTTON_MINUS | 346 | VPAD_BUTTON_HOME | VPAD_BUTTON_TV | 347 | VPAD_BUTTON_STICK_L | VPAD_BUTTON_STICK_R | 348 | VPAD_BUTTON_SYNC; 349 | 350 | 351 | DECL_FUNCTION(int32_t, VPADRead, 352 | VPADChan channel, 353 | VPADStatus* buf, 354 | uint32_t count, 355 | VPADReadError* error) 356 | { 357 | auto result = real_VPADRead(channel, buf, count, error); 358 | 359 | if (error && *error == VPAD_READ_INVALID_CONTROLLER) 360 | if (channel >= 0 && channel < 2) 361 | vpad_states[channel].attached.store(false, 362 | std::memory_order::release); 363 | 364 | if (result <= 0) 365 | return result; 366 | 367 | if (error && *error != VPAD_READ_SUCCESS) 368 | return result; 369 | 370 | if (!cfg::enabled.value) 371 | return result; 372 | 373 | vpad_states[channel].attached.store(true, std::memory_order::release); 374 | 375 | // Don't bother doing anything else if the config menu is open. 376 | WUPSConfigAPIMenuStatus menu_status{}; 377 | WUPSConfigAPI_Menu_GetStatus(&menu_status); 378 | if (menu_status == WUPSCONFIG_API_MENU_STATUS_OPENED) 379 | return result; 380 | 381 | if (cfg::button_rate.value) { 382 | // Note: when proc mode is loose, all button samples are identical to the most recent. 383 | bool is_loose = !VPADGetButtonProcMode(channel); 384 | int num_samples = is_loose ? 1 : result; 385 | 386 | unsigned counter = 0; 387 | for (int idx = num_samples - 1; idx >= 0; --idx) 388 | counter += std::popcount(buf[idx].trigger & vpad_mask); 389 | 390 | if (counter) 391 | button_presses.fetch_add(counter, std::memory_order::relaxed); 392 | } 393 | 394 | if (cfg::battery.value) 395 | vpad_states[channel].battery.store(buf[0].battery, std::memory_order::relaxed); 396 | 397 | return result; 398 | } 399 | 400 | WUPS_MUST_REPLACE(VPADRead, WUPS_LOADER_LIBRARY_VPAD, VPADRead); 401 | 402 | 403 | DECL_FUNCTION(void, __VPADBASEAttachCallback, 404 | CCRCDCRegisterCallbackData *data, 405 | unsigned status) 406 | { 407 | real___VPADBASEAttachCallback(data, status); 408 | if (data) 409 | #if 1 410 | vpad_states[data->chan].attached.store(!!status, std::memory_order::release); 411 | #else 412 | vpad_states[data->chan].attached.store(!!data->attached, std::memory_order::release); 413 | #endif 414 | } 415 | 416 | WUPS_MUST_REPLACE_PHYSICAL(__VPADBASEAttachCallback, 417 | (0x0200146c + 0x31000000 - 0xee0100), 418 | (0x0200146c - 0x00000000 - 0xee0100)); 419 | 420 | 421 | DECL_FUNCTION(void, WPADRead, 422 | WPADChan channel, 423 | WPADStatus* status) 424 | { 425 | real_WPADRead(channel, status); 426 | 427 | if (!status) 428 | return; 429 | if (status->error) 430 | return; 431 | if (!cfg::enabled.value) 432 | return; 433 | 434 | // Don't bother doing anything else if the config menu is open. 435 | WUPSConfigAPIMenuStatus menu_status{}; 436 | WUPSConfigAPI_Menu_GetStatus(&menu_status); 437 | if (menu_status == WUPSCONFIG_API_MENU_STATUS_OPENED) 438 | return; 439 | 440 | if (channel < 0 || channel >= wpad_states.size()) 441 | return; 442 | 443 | if (cfg::button_rate.value) { 444 | 445 | auto& wiimote = wpad_states[channel]; 446 | 447 | wiimote.update_ext_type(status->extensionType); 448 | 449 | using std::popcount; 450 | unsigned counter = 0; 451 | 452 | switch (status->extensionType) { 453 | case WPAD_EXT_CORE: 454 | case WPAD_EXT_MPLUS: 455 | case WPAD_EXT_NUNCHUK: 456 | case WPAD_EXT_MPLUS_NUNCHUK: 457 | wiimote.core.update(status->buttons); 458 | counter += popcount(wiimote.core.trigger); 459 | break; 460 | 461 | case WPAD_EXT_CLASSIC: 462 | case WPAD_EXT_MPLUS_CLASSIC: 463 | wiimote.core.update(status->buttons); 464 | wiimote.ext.update(reinterpret_cast(status)->buttons); 465 | counter += popcount(wiimote.core.trigger); 466 | counter += popcount(wiimote.ext.trigger); 467 | break; 468 | 469 | case WPAD_EXT_PRO_CONTROLLER: 470 | wiimote.ext.update(reinterpret_cast(status)->buttons); 471 | counter += popcount(wiimote.ext.trigger); 472 | break; 473 | } 474 | 475 | if (counter) 476 | button_presses.fetch_add(counter, std::memory_order::relaxed); 477 | 478 | } 479 | 480 | if (cfg::battery.value) { 481 | // The only way to track the charging/wired status of Pro Controller is to 482 | // peek into the WPADStatusProController fields. 483 | if (status->extensionType == WPAD_EXT_PRO_CONTROLLER) { 484 | auto pro_status = reinterpret_cast(status); 485 | unsigned flags = 0; 486 | if (pro_status->charging) 487 | flags |= PRO_BATTERY_CHARGING; 488 | if (pro_status->wired) 489 | flags |= PRO_BATTERY_WIRED; 490 | pro_power[channel] = flags; 491 | } 492 | } 493 | } 494 | 495 | WUPS_MUST_REPLACE(WPADRead, WUPS_LOADER_LIBRARY_PADSCORE, WPADRead); 496 | 497 | } // namespace pad_mon 498 | -------------------------------------------------------------------------------- /src/pad_mon.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef PAD_MON_HPP 10 | #define PAD_MON_HPP 11 | 12 | #include "out_span.hpp" 13 | 14 | 15 | namespace pad_mon { 16 | 17 | void 18 | initialize(); 19 | 20 | void 21 | finalize(); 22 | 23 | void 24 | reset(); 25 | 26 | void 27 | get_report(out_span& out, 28 | float dt); 29 | 30 | } // namespace pad_mon 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/time_mon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | /* 10 | * Time Monitoring 11 | * 12 | * Just show a clock. This helps users to not lose track of time while playing. 13 | */ 14 | 15 | #include 16 | 17 | #include 18 | 19 | #include "time_mon.hpp" 20 | 21 | #include "cfg.hpp" 22 | #include "utils.hpp" 23 | 24 | 25 | namespace time_mon { 26 | 27 | OSTime app_start_time; 28 | 29 | 30 | void 31 | initialize() 32 | {} 33 | 34 | 35 | void 36 | finalize() 37 | {} 38 | 39 | 40 | void 41 | reset() 42 | {} 43 | 44 | 45 | void 46 | on_application_start() 47 | { 48 | app_start_time = OSGetSystemTime(); 49 | } 50 | 51 | 52 | void 53 | get_report(out_span& out, 54 | float) 55 | { 56 | const char* separator = ""; 57 | 58 | if (cfg::time.value) { 59 | 60 | OSTime now = OSGetTime(); 61 | OSCalendarTime cal; 62 | OSTicksToCalendarTime(now, &cal); 63 | 64 | int h = cal.tm_hour; 65 | int m = cal.tm_min; 66 | 67 | if (cfg::time_24h.value) 68 | out.printf("%02d:%02d", h, m); 69 | else { 70 | const char* suffix = h >=12 ? "pm" : "am"; 71 | h = (h + 11) % 12 + 1; 72 | out.printf("%d:%02d %s", 73 | h, m, suffix); 74 | } 75 | separator = utils::field_separator; 76 | } 77 | 78 | if (cfg::uptime.value) { 79 | float up_seconds = float(OSGetSystemTime()) / OSTimerClockSpeed; 80 | out.printf("%sup: ", separator); 81 | utils::format_seconds(out, up_seconds); 82 | separator = utils::field_separator; 83 | } 84 | 85 | if (cfg::play_time.value) { 86 | OSTime play_duration = OSGetSystemTime() - app_start_time; 87 | float play_seconds = float(play_duration) / OSTimerClockSpeed; 88 | out.printf("%splay: ", separator); 89 | utils::format_seconds(out, play_seconds); 90 | } 91 | } 92 | 93 | } // namespace time_mon 94 | -------------------------------------------------------------------------------- /src/time_mon.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef TIME_MON_HPP 10 | #define TIME_MON_HPP 11 | 12 | #include "out_span.hpp" 13 | 14 | 15 | namespace time_mon { 16 | 17 | void 18 | initialize(); 19 | 20 | void 21 | finalize(); 22 | 23 | void 24 | reset(); 25 | 26 | void 27 | on_application_start(); 28 | 29 | void 30 | get_report(out_span& out, 31 | float dt); 32 | 33 | } // namespace time_mon 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #include // clamp() 10 | #include 11 | #include 12 | #include 13 | 14 | #include "utils.hpp" 15 | 16 | 17 | using std::size_t; 18 | 19 | 20 | namespace utils { 21 | 22 | const char* 23 | percent_to_bar(float p) 24 | { 25 | static const std::array bars{ 26 | "\u3000", 27 | "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" 28 | }; 29 | long idx = std::floor(bars.size() * p / 100.0); 30 | idx = std::clamp(idx, 0l, bars.size() - 1); 31 | return bars[idx]; 32 | } 33 | 34 | 35 | void 36 | format_bytes(out_span& out, float bytes) 37 | { 38 | static const std::array suffixes = { 39 | "B", 40 | "KiB", 41 | "MiB", 42 | "GiB", 43 | "TiB" 44 | }; 45 | 46 | size_t suffix_idx = 0; 47 | while (bytes >= 768) { 48 | if (++suffix_idx >= suffixes.size()) { 49 | --suffix_idx; 50 | break; 51 | } 52 | bytes /= 1024; 53 | } 54 | 55 | out.printf("%.1f %s", bytes, suffixes[suffix_idx]); 56 | } 57 | 58 | 59 | void 60 | format_seconds(out_span& out, float seconds) 61 | { 62 | using std::div; 63 | using std::lround; 64 | 65 | if (seconds < 0.75) { 66 | out.printf("%ld ms", lround(seconds * 1000)); 67 | return; 68 | } 69 | 70 | // If less than a minute. 71 | if (seconds < 60) { 72 | out.printf("%.1f s", seconds); 73 | return; 74 | } 75 | 76 | // If less than an hour. 77 | if (seconds < 60 * 60) { 78 | auto min_sec = div(lround(seconds), 60l); 79 | out.printf("%ld min", min_sec.quot); 80 | if (min_sec.rem) // avoid showing "0 s" 81 | out.printf(", %ld s", min_sec.rem); 82 | return; 83 | } 84 | 85 | // If less than a day. 86 | if (seconds < 24 * 60 * 60) { 87 | auto minutes = lround(seconds) / 60l; 88 | auto hr_min = div(minutes, 60l); 89 | out.printf("%ld h", hr_min.quot); 90 | if (hr_min.rem) // avoid showing "0 min" 91 | out.printf(", %ld min", hr_min.rem); 92 | return; 93 | } 94 | 95 | // General case: a day or more. 96 | auto minutes = lround(seconds) / 60l; 97 | auto hr_min = div(minutes, 60l); 98 | auto d_h = div(hr_min.quot, 24l); 99 | 100 | out.printf("%ld d", d_h.quot); 101 | if (d_h.rem) // avoid showing "0 h" 102 | out.printf(", %ld h", d_h.rem); 103 | if (hr_min.rem) // avoid showing "0 min" 104 | out.printf(", %ld min", hr_min.rem); 105 | 106 | } 107 | 108 | } // namespace utils 109 | -------------------------------------------------------------------------------- /src/utils.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Papaya-HUD - a HUD plugin for Aroma. 3 | * 4 | * Copyright (C) 2025 Daniel K. O. 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | */ 8 | 9 | #ifndef UTILS_HPP 10 | #define UTILS_HPP 11 | 12 | #include 13 | 14 | #include "out_span.hpp" 15 | 16 | 17 | namespace utils { 18 | 19 | constexpr const char* field_separator = "┃"; 20 | 21 | 22 | const char* 23 | percent_to_bar(float p); 24 | 25 | void 26 | format_bytes(out_span& dst, 27 | float bytes); 28 | 29 | void 30 | format_seconds(out_span& dst, 31 | float seconds); 32 | 33 | } // namespace utils 34 | 35 | #endif 36 | --------------------------------------------------------------------------------