├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── .libdragon └── config.json ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── Makefile ├── README.md ├── doc └── mimi.png ├── fs └── font │ ├── bold.sprite │ ├── bold_charwidths.bin │ ├── medium.sprite │ └── medium_charwidths.bin ├── gfx ├── point.png ├── stick_0.png ├── stick_1.png ├── stick_2.png ├── stick_3.png ├── stick_4.png ├── stick_5.png ├── stick_6.png ├── stick_7.png └── stick_neutral.png └── src ├── colors.h ├── drawing.c ├── drawing.h ├── input.c ├── input.h ├── main.c ├── oscilloscope.c ├── oscilloscope.h ├── range_live.c ├── range_live.h ├── range_test.c ├── range_test.h ├── text.c ├── text.h └── util.h /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: wermi 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .tmp/ 3 | *.z64 4 | fs/gfx/*.sprite 5 | .*.sw* 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libdragon"] 2 | path = libdragon 3 | url = https://github.com/DragonMinded/libdragon 4 | branch = trunk 5 | -------------------------------------------------------------------------------- /.libdragon/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "imageName": "ghcr.io/dragonminded/libdragon:latest", 3 | "vendorDirectory": "libdragon", 4 | "vendorStrategy": "submodule" 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.errorSquiggles": "Disabled" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "make", 8 | "type": "shell", 9 | "command": "libdragon make", 10 | "problemMatcher": [], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wermi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | V=1 2 | SOURCE_DIR=src 3 | BUILD_DIR=build 4 | include $(N64_INST)/include/n64.mk 5 | ROM_VERSION=$(shell git describe --always --match 'NOT A TAG') 6 | NAME=mimi-$(ROM_VERSION) 7 | ROMNAME=$(NAME).z64 8 | REPO_URL=github.com/wermipls/mimi 9 | FS=$(BUILD_DIR)/data.dfs 10 | 11 | N64_CFLAGS += -DROM_VERSION=\""$(ROM_VERSION)"\" 12 | N64_CFLAGS += -DREPO_URL=\""$(REPO_URL)"\" 13 | 14 | all: $(ROMNAME) 15 | .PHONY: all 16 | 17 | build/main.o: .FORCE 18 | .FORCE: 19 | 20 | SRCS = $(wildcard $(SOURCE_DIR)/*.c) 21 | OBJS = $(SRCS:$(SOURCE_DIR)/%.c=$(BUILD_DIR)/%.o) 22 | 23 | $(ROMNAME): N64_ROM_TITLE="mimi controller test" 24 | $(ROMNAME): $(FS) 25 | 26 | $(FS): $(wildcard fs/*) $(wildcard gfx/*) 27 | mkdir -p fs/gfx/ 28 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_0.png fs/gfx/stick_0.sprite 29 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_1.png fs/gfx/stick_1.sprite 30 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_2.png fs/gfx/stick_2.sprite 31 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_3.png fs/gfx/stick_3.sprite 32 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_4.png fs/gfx/stick_4.sprite 33 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_5.png fs/gfx/stick_5.sprite 34 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_6.png fs/gfx/stick_6.sprite 35 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_7.png fs/gfx/stick_7.sprite 36 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/stick_neutral.png fs/gfx/stick_neutral.sprite 37 | $(N64_ROOTDIR)/bin/mksprite 32 gfx/point.png fs/gfx/point.sprite 38 | $(N64_MKDFS) $@ fs 39 | 40 | $(BUILD_DIR)/$(NAME).elf: $(OBJS) 41 | 42 | clean: 43 | rm -f $(BUILD_DIR)/* *.z64 44 | .PHONY: clean 45 | 46 | -include $(wildcard $(BUILD_DIR)/*.d) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mimi 2 | ![Example screenshot](doc/mimi.png) 3 | 4 | **mimi** is a controller test rom for Nintendo 64. It's heavily inspired by [sanni's controllertest](https://github.com/sanni/controllertest/tree/master/N64-Port) and [max257612's fork thereof](https://github.com/max257612/controllertest), however it is written completely from scratch. Differences include improved graphics, ability to take multiple measurements, improved judgement of ranges, additional information on result screen, more extensible codebase, as well as various UX improvements. 5 | 6 | If you use this software commercially (e.g. to provide measurements for the controllers you sell), consider buying me a Ko-fi. 7 | 8 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B37GRPD) 9 | 10 | ## Download 11 | Latest version of the ROM can be found [here](https://github.com/wermipls/mimi/releases). 12 | 13 | ## Basic controls 14 | In the main menu: 15 | * D-Pad - select option 16 | * A - confirm selection 17 | 18 | On the help screen: 19 | * D-Pad Left/Right or L/R - change page 20 | * A/B - return to main menu 21 | 22 | On the range test result screen: 23 | * L/R - switch between range comparisons 24 | * D-Pad Up/Down - switch between measurements 25 | * D-Pad Left/Right - switch between example ranges and result measurements 26 | * Z - change zoom 27 | * Start - return to main menu 28 | 29 | Additional informations on usage can be found in the help screen built into the ROM. 30 | 31 | ## Building 32 | To build the ROM, you will need to have [libdragon](https://libdragon.dev/) set up. 33 | 34 | Clone the repository (including submodules) and navigate to the directory: 35 | ``` 36 | git clone --recurse-submodules 37 | cd mimi 38 | ``` 39 | 40 | For initial build: 41 | ``` 42 | libdragon init 43 | libdragon make 44 | ``` 45 | 46 | After that, the ROM should be possible to compile with just `libdragon make`. 47 | 48 | ## Credits 49 | * wermi - main code and graphics 50 | * kolunio - testing, minor improvements and code review 51 | 52 | Special thanks to adelyn, alaris, Bailey, billy, egasyelir, GiBoss, jwaterman, Kyman, Manama, MontyVR, nim, Tabascoth, Taki and tayyip. 53 | 54 | ### 3rd party assets 55 | * [Enter Command font by Font End Dev](https://fontenddev.com/fonts/enter-command/), licensed under CC BY 4.0 56 | 57 | ## License 58 | MIT 59 | -------------------------------------------------------------------------------- /doc/mimi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/doc/mimi.png -------------------------------------------------------------------------------- /fs/font/bold.sprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/fs/font/bold.sprite -------------------------------------------------------------------------------- /fs/font/bold_charwidths.bin: -------------------------------------------------------------------------------- 1 |   2 |   -------------------------------------------------------------------------------- /fs/font/medium.sprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/fs/font/medium.sprite -------------------------------------------------------------------------------- /fs/font/medium_charwidths.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /gfx/point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/point.png -------------------------------------------------------------------------------- /gfx/stick_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_0.png -------------------------------------------------------------------------------- /gfx/stick_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_1.png -------------------------------------------------------------------------------- /gfx/stick_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_2.png -------------------------------------------------------------------------------- /gfx/stick_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_3.png -------------------------------------------------------------------------------- /gfx/stick_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_4.png -------------------------------------------------------------------------------- /gfx/stick_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_5.png -------------------------------------------------------------------------------- /gfx/stick_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_6.png -------------------------------------------------------------------------------- /gfx/stick_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_7.png -------------------------------------------------------------------------------- /gfx/stick_neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wermipls/mimi/7dcf0574577d36313ff10df9d2de986157f18413/gfx/stick_neutral.png -------------------------------------------------------------------------------- /src/colors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define COLOR_FOREGROUND graphics_make_color(255, 255, 255, 255) 6 | #define COLOR_BACKGROUND graphics_make_color(0, 0, 0, 255) 7 | -------------------------------------------------------------------------------- /src/drawing.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "drawing.h" 4 | #include "util.h" 5 | 6 | static inline uint32_t color_alpha(uint32_t c, uint8_t i) 7 | { 8 | return (c & 0xFFFFFF00) | i; 9 | } 10 | 11 | static inline uint8_t gammac(int i) 12 | { 13 | static uint8_t gamma_lut[] = { 14 | 0x00, 0x17, 0x1c, 0x20, 0x23, 0x25, 0x27, 0x2a, 0x2d, 0x30, 0x32, 0x35, 0x37, 0x3a, 0x3c, 0x3e, 15 | 0x40, 0x42, 0x44, 0x46, 0x47, 0x49, 0x4b, 0x4d, 0x4e, 0x50, 0x51, 0x53, 0x55, 0x56, 0x57, 0x59, 16 | 0x5a, 0x5c, 0x5d, 0x5e, 0x60, 0x61, 0x62, 0x64, 0x65, 0x66, 0x67, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 17 | 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 18 | 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 19 | 0x8f, 0x90, 0x91, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 20 | 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa4, 0xa5, 0xa6, 0xa7, 0xa7, 0xa8, 21 | 0xa9, 0xaa, 0xaa, 0xab, 0xac, 0xad, 0xad, 0xae, 0xaf, 0xb0, 0xb0, 0xb1, 0xb2, 0xb3, 0xb3, 0xb4, 22 | 0xb5, 0xb5, 0xb6, 0xb7, 0xb7, 0xb8, 0xb9, 0xba, 0xba, 0xbb, 0xbc, 0xbc, 0xbd, 0xbe, 0xbe, 0xbf, 23 | 0xc0, 0xc0, 0xc1, 0xc2, 0xc2, 0xc3, 0xc4, 0xc4, 0xc5, 0xc6, 0xc6, 0xc7, 0xc7, 0xc8, 0xc9, 0xc9, 24 | 0xca, 0xcb, 0xcb, 0xcc, 0xcc, 0xcd, 0xce, 0xce, 0xcf, 0xd0, 0xd0, 0xd1, 0xd1, 0xd2, 0xd3, 0xd3, 25 | 0xd4, 0xd4, 0xd5, 0xd6, 0xd6, 0xd7, 0xd7, 0xd8, 0xd9, 0xd9, 0xda, 0xda, 0xdb, 0xdc, 0xdc, 0xdd, 26 | 0xdd, 0xde, 0xde, 0xdf, 0xe0, 0xe0, 0xe1, 0xe1, 0xe2, 0xe2, 0xe3, 0xe4, 0xe4, 0xe5, 0xe5, 0xe6, 27 | 0xe6, 0xe7, 0xe7, 0xe8, 0xe9, 0xe9, 0xea, 0xea, 0xeb, 0xeb, 0xec, 0xec, 0xed, 0xed, 0xee, 0xee, 28 | 0xef, 0xf0, 0xf0, 0xf1, 0xf1, 0xf2, 0xf2, 0xf3, 0xf3, 0xf4, 0xf4, 0xf5, 0xf5, 0xf6, 0xf6, 0xf7, 29 | 0xf7, 0xf8, 0xf8, 0xf9, 0xf9, 0xfa, 0xfa, 0xfb, 0xfb, 0xfc, 0xfc, 0xfd, 0xfd, 0xfe, 0xfe, 0xff, 30 | }; 31 | 32 | return gamma_lut[i]; 33 | } 34 | 35 | void draw_aa_line(display_context_t ctx, int x0, int y0, int x1, int y1, uint32_t c) 36 | { 37 | x0 = smax(smin(x0, 319), 0); 38 | x1 = smax(smin(x1, 319), 0); 39 | y0 = smax(smin(y0, 239), 0); 40 | y1 = smax(smin(y1, 239), 0); 41 | 42 | if (x0 == x1 || y0 == y1) { 43 | graphics_draw_line(ctx, x0, y0, x1, y1, c); 44 | return; 45 | } 46 | 47 | if (x0 > x1) { 48 | int tmp = x0; 49 | x0 = x1; 50 | x1 = tmp; 51 | tmp = y0; 52 | y0 = y1; 53 | y1 = tmp; 54 | } 55 | 56 | int h = x1 - x0; 57 | int v = y1 - y0; 58 | 59 | bool rising = v < 0; 60 | v = abs(v); 61 | bool steep = h < v; 62 | int inc = rising ? -1 : 1; 63 | 64 | if (steep) { 65 | uint16_t f = ((uint32_t)h << 16) / v; 66 | int x = x0; 67 | uint16_t a = 0; 68 | 69 | for (int y = y0; y != y1; y += inc) { 70 | uint8_t i = (0xFFFF - a) >> 8; 71 | graphics_draw_pixel_trans(ctx, x, y, color_alpha(c, gammac(i))); 72 | i = a >> 8; 73 | graphics_draw_pixel_trans(ctx, x+1, y, color_alpha(c, gammac(i))); 74 | uint16_t b = a + f; 75 | if (b <= a) { 76 | x++; 77 | } 78 | a = b; 79 | } 80 | } else { 81 | uint16_t f = ((uint32_t)v << 16) / h; 82 | int y = y0; 83 | uint16_t a = 0; 84 | 85 | for (int x = x0; x < x1; x++) { 86 | uint8_t i = (0xFFFF - a) >> 8; 87 | graphics_draw_pixel_trans(ctx, x, y, color_alpha(c, gammac(i))); 88 | i = a >> 8; 89 | graphics_draw_pixel_trans(ctx, x, y+inc, color_alpha(c, gammac(i))); 90 | uint16_t b = a + f; 91 | if (b <= a) { 92 | y += inc; 93 | } 94 | a = b; 95 | } 96 | } 97 | 98 | graphics_draw_pixel(ctx, x1, y1, c); 99 | } 100 | -------------------------------------------------------------------------------- /src/drawing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void draw_aa_line(display_context_t ctx, int x0, int y0, int x1, int y1, uint32_t c); 6 | -------------------------------------------------------------------------------- /src/input.c: -------------------------------------------------------------------------------- 1 | #include "input.h" 2 | 3 | struct controller_data get_keys_down_filtered() 4 | { 5 | struct controller_data d = get_keys_down(); 6 | for (int i = 0; i < 4; i++) { 7 | if (d.c[0].err != ERROR_NONE) { 8 | d.c[0].data = 0; 9 | } 10 | } 11 | return d; 12 | } 13 | -------------------------------------------------------------------------------- /src/input.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct controller_data get_keys_down_filtered(); 6 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "range_test.h" 6 | #include "range_live.h" 7 | #include "oscilloscope.h" 8 | #include "text.h" 9 | #include "colors.h" 10 | #include "input.h" 11 | 12 | enum Screen 13 | { 14 | SCR_MAIN_MENU, 15 | SCR_HELP, 16 | SCR_ABOUT, 17 | SCR_RANGE_TEST, 18 | SCR_RANGE_RESULT, 19 | SCR_LIVE, 20 | SCR_OSCOPE, 21 | }; 22 | 23 | void reset_handler(exception_t *ex) 24 | { 25 | if (ex->type != EXCEPTION_TYPE_RESET) { 26 | exception_default_handler(ex); 27 | return; 28 | } 29 | 30 | abort(); 31 | } 32 | 33 | int main(void) 34 | { 35 | display_init(RESOLUTION_320x240, DEPTH_32_BPP, 2, GAMMA_NONE, ANTIALIAS_RESAMPLE); 36 | controller_init(); 37 | dfs_init(DFS_DEFAULT_LOCATION); 38 | 39 | text_init(); 40 | 41 | console_set_debug(true); 42 | register_exception_handler(reset_handler); 43 | 44 | enum Screen current_screen = 0; 45 | 46 | struct StickAngles result[9]; 47 | display_context_t ctx; 48 | int sample_count = -1; 49 | int is_unsaved_result = 0; 50 | 51 | for (;;) { 52 | switch (current_screen) 53 | { 54 | case SCR_MAIN_MENU: 55 | static int menu_selection = 0; 56 | 57 | text_set_line_height(11); 58 | for (;;) { 59 | while ((ctx = display_lock()) == 0) {} 60 | 61 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 62 | 63 | graphics_set_color(COLOR_FOREGROUND, 0); 64 | text_set_font(FONT_BOLD); 65 | text_draw(ctx, 32, 24, "mimi git-" ROM_VERSION " (built on " __DATE__ ")", ALIGN_LEFT); 66 | 67 | static const char *options[] = { 68 | "Range test (1 sample)", 69 | "Range test (3 samples)", 70 | "Range test (5 samples)", 71 | "Display last range result", 72 | "Live range display", 73 | "Oscilloscope display", 74 | "Help", 75 | "About", 76 | }; 77 | 78 | int menu_options = sizeof(options)/sizeof(char*); 79 | text_set_font(FONT_MEDIUM); 80 | 81 | for (int i = 0; i < menu_options; i++) { 82 | int x = 42; 83 | if (i == menu_selection) { 84 | text_draw(ctx, x - 10, 44 + i*11, ">", ALIGN_LEFT); 85 | } 86 | 87 | text_draw(ctx, x, 44 + i*11, options[i], ALIGN_LEFT); 88 | } 89 | 90 | display_show(ctx); 91 | 92 | controller_scan(); 93 | struct controller_data cdata = get_keys_down_filtered(); 94 | 95 | if (cdata.c[0].A) { 96 | switch (menu_selection) 97 | { 98 | case 0: 99 | sample_count = 1; 100 | current_screen = SCR_RANGE_TEST; 101 | break; 102 | case 1: 103 | sample_count = 3; 104 | current_screen = SCR_RANGE_TEST; 105 | break; 106 | case 2: 107 | sample_count = 5; 108 | current_screen = SCR_RANGE_TEST; 109 | break; 110 | case 3: 111 | if (sample_count > 0) { 112 | current_screen = SCR_RANGE_RESULT; 113 | } 114 | break; 115 | case 4: 116 | current_screen = SCR_LIVE; 117 | break; 118 | case 5: 119 | current_screen = SCR_OSCOPE; 120 | break; 121 | case 6: 122 | current_screen = SCR_HELP; 123 | break; 124 | case 7: 125 | current_screen = SCR_ABOUT; 126 | break; 127 | } 128 | } 129 | 130 | if (current_screen != SCR_MAIN_MENU) { 131 | break; 132 | } 133 | 134 | if (cdata.c[0].up) { 135 | menu_selection--; 136 | if (menu_selection < 0) menu_selection = menu_options - 1; 137 | } else if (cdata.c[0].down) { 138 | menu_selection++; 139 | if (menu_selection >= menu_options) menu_selection = 0; 140 | } 141 | } 142 | break; 143 | case SCR_ABOUT: 144 | text_set_line_height(11); 145 | for (;;) { 146 | while ((ctx = display_lock()) == 0) {} 147 | 148 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 149 | 150 | graphics_set_color(COLOR_FOREGROUND, 0); 151 | text_set_font(FONT_BOLD); 152 | 153 | text_draw(ctx, 32, 24, "About", ALIGN_LEFT); 154 | 155 | text_set_font(FONT_MEDIUM); 156 | 157 | text_draw_wordwrap(ctx, 32, 44, 320-64, 158 | "mimi controller test ROM by wermi\n" 159 | "version " ROM_VERSION ", built on " __DATE__ "\n\n" 160 | REPO_URL "\n\n" 161 | "Enter Command font by Font End Dev (fontenddev.com), " 162 | "licensed under CC BY 4.0\n\n" 163 | "This ROM is heavily inspired by sanni's controllertest " 164 | "port for N64, as well as max257612's fork of it, however " 165 | "it is written completely from scratch.\n\n" 166 | ); 167 | 168 | display_show(ctx); 169 | 170 | controller_scan(); 171 | struct controller_data cdata = get_keys_down_filtered(); 172 | 173 | if (cdata.c[0].A || cdata.c[0].B || cdata.c[0].start) { 174 | current_screen = SCR_MAIN_MENU; 175 | break; 176 | } 177 | } 178 | break; 179 | case SCR_HELP: 180 | const char *page_names[] = { 181 | "Basic controls", 182 | "Basic controls cont.", 183 | "Range testing", 184 | "Range testing cont.", 185 | "Range testing cont.", 186 | "Live range display", 187 | "Oscilloscope display", 188 | }; 189 | const int pages = sizeof(page_names) / sizeof(char*); 190 | int page = 0; 191 | 192 | text_set_line_height(11); 193 | for (;;) { 194 | while ((ctx = display_lock()) == 0) {} 195 | 196 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 197 | graphics_set_color(COLOR_FOREGROUND, 0); 198 | 199 | text_set_font(FONT_BOLD); 200 | text_draw(ctx, 32, 24, page_names[page], ALIGN_LEFT); 201 | 202 | if (page < pages - 1) { 203 | text_draw(ctx, 320-32, 205, "Next page >>", ALIGN_RIGHT); 204 | } 205 | 206 | if (page > 0) { 207 | text_draw(ctx, 32, 205, "<< Prev. page", ALIGN_LEFT); 208 | } 209 | 210 | text_set_font(FONT_MEDIUM); 211 | 212 | switch (page) 213 | { 214 | case 0: 215 | text_set_font(FONT_MEDIUM); 216 | text_draw(ctx, 32, 44 + 11*0, 217 | "* D-Pad Left/Right or L/R - change page\n" 218 | "* A/B - return to main menu", ALIGN_LEFT); 219 | text_set_font(FONT_BOLD); 220 | text_draw(ctx, 32, 44 + 11*3, 221 | "In the main menu:", ALIGN_LEFT); 222 | text_set_font(FONT_MEDIUM); 223 | text_draw(ctx, 32, 44 + 11*4, 224 | "* D-Pad - select option\n" 225 | "* A - confirm selection\n", ALIGN_LEFT); 226 | text_set_font(FONT_BOLD); 227 | text_draw(ctx, 32, 44 + 11*7, 228 | "On the range test result screen:", ALIGN_LEFT); 229 | text_set_font(FONT_MEDIUM); 230 | text_draw(ctx, 32, 44 + 11*8, 231 | "* L/R - switch between range comparisons\n" 232 | "* D-Pad Up/Down - switch between measurements\n" 233 | "* D-Pad Left/Right - switch between\n" 234 | " example ranges and result measurements\n" 235 | "* Z - change zoom\n" 236 | "* Start - return to main menu\n", ALIGN_LEFT); 237 | break; 238 | case 1: 239 | text_set_font(FONT_BOLD); 240 | text_draw_wordwrap(ctx, 32, 44 + (11 * 0), 320-64, 241 | "On the live range testing screen:\n"); 242 | text_set_font(FONT_MEDIUM); 243 | text_draw_wordwrap(ctx, 32, 44 + (11 * 1), 320-64, 244 | "* A - toggle history display\n" 245 | "* B - clear history display\n" 246 | "* Z - change zoom\n" 247 | "* L/R, D-Pad Left/Right - cycle example ranges\n" 248 | "* Start - return to main menu\n"); 249 | text_set_font(FONT_BOLD); 250 | text_draw_wordwrap(ctx, 32, 44 + (11 * 7), 320-64, 251 | "On the oscilloscope screen:\n"); 252 | text_set_font(FONT_MEDIUM); 253 | text_draw_wordwrap(ctx, 32, 44 + (11 * 8), 320-64, 254 | "* Start - return to main menu\n"); 255 | break; 256 | case 2: 257 | text_draw_wordwrap(ctx, 32, 44, 320-64, 258 | "User can take one or more measurements of the " 259 | "analog values. More measurements help even out " 260 | "any variations caused either by user error or " 261 | "stick inconsistency. A median is taken from " 262 | "all measurements and gets displayed in light blue " 263 | "as the default measurement. The remaining " 264 | "measurements are drawn in the background in gray " 265 | "to visualise deviations.\n\n" 266 | 267 | "Optionally, a comparison to an example range " 268 | "(displayed in green) can be enabled, which helps " 269 | "to judge the controller's range." 270 | ); 271 | break; 272 | case 3: 273 | text_draw_wordwrap(ctx, 32, 44, 320-64, 274 | "The measurement display can also be overriden with " 275 | "one of the example ones, to let user view the expected " 276 | "values and angles. The measurement is displayed in pink " 277 | "to differentiate between actual measurements.\n\n" 278 | 279 | "Some particularly bad controllers can have overly " 280 | "high range, which would not fit the screen. Zoom " 281 | "will be automatically changed to 75\% to compensate " 282 | "in those cases, but the setting can be manually " 283 | "overriden by user." 284 | ); 285 | break; 286 | case 4: 287 | text_draw_wordwrap(ctx, 32, 44, 320-64, 288 | "Absolute analog values for each notch are displayed " 289 | "on the right of the screen, as well as angles " 290 | "for each diagonal. The values have colors assigned " 291 | "based on following criteria:\n" 292 | "* green - 80+ magnitude OR w/in 1" SYMBOL_DEGREES " from 45" SYMBOL_DEGREES "\n" 293 | "* lime - 75+ magnitude OR w/in 3" SYMBOL_DEGREES " from 45" SYMBOL_DEGREES "\n" 294 | "* orange - 70+ magnitude OR w/in 5" SYMBOL_DEGREES " from 45" SYMBOL_DEGREES "\n" 295 | "* red - any other value\n\n" 296 | 297 | "The diagonal magnitude cutoffs are 9/8th times " 298 | "the cardinal ones to compensate for higher " 299 | "magnitude diagonals on original N64 controllers." 300 | ); 301 | break; 302 | case 5: 303 | text_draw_wordwrap(ctx, 32, 44, 320-64, 304 | "Displays live X/Y values on a graph using ideal " 305 | "OEM or Hori values as an overlay. Displays " 306 | "the most recent 1024 values in blue, and the " 307 | "current X/Y values as integers.\n\n" 308 | ); 309 | break; 310 | case 6: 311 | text_draw_wordwrap(ctx, 32, 44, 320-64, 312 | "Displays live X/Y values on an oscilloscope-style " 313 | "display. Useful for identifying skips and " 314 | "snapback issues.\n\n" 315 | ); 316 | break; 317 | } 318 | 319 | 320 | display_show(ctx); 321 | 322 | controller_scan(); 323 | struct controller_data cdata = get_keys_down_filtered(); 324 | 325 | if (cdata.c[0].A || cdata.c[0].B || cdata.c[0].start) { 326 | current_screen = SCR_MAIN_MENU; 327 | break; 328 | } 329 | 330 | if (cdata.c[0].right || cdata.c[0].R) { 331 | if (page < pages - 1) page++; 332 | } 333 | 334 | if (cdata.c[0].left || cdata.c[0].L) { 335 | if (page > 0) page--; 336 | } 337 | } 338 | break; 339 | case SCR_RANGE_TEST: 340 | if (is_unsaved_result) { 341 | for (;;) { 342 | while ((ctx = display_lock()) == 0) {} 343 | 344 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 345 | 346 | graphics_set_color(COLOR_FOREGROUND, 0); 347 | text_set_font(FONT_BOLD); 348 | text_draw(ctx, 160, 80, "Previous result will be discarded.\nAre you sure?", ALIGN_CENTER); 349 | text_set_font(FONT_MEDIUM); 350 | text_draw(ctx, 160, 160, "Press Start to continue or B to cancel.", ALIGN_CENTER); 351 | 352 | display_show(ctx); 353 | 354 | controller_scan(); 355 | struct controller_data cdata = get_keys_down_filtered(); 356 | 357 | if (cdata.c[0].start) { 358 | is_unsaved_result = 0; 359 | break; 360 | } else if (cdata.c[0].B) { 361 | break; 362 | } 363 | } 364 | } 365 | if (is_unsaved_result) { 366 | current_screen = SCR_MAIN_MENU; 367 | break; 368 | } 369 | 370 | for (int i = 0; i < sample_count; i++) { 371 | test_angles(&result[i], i+1); 372 | } 373 | is_unsaved_result = 1; 374 | current_screen = SCR_RANGE_RESULT; 375 | break; 376 | case SCR_RANGE_RESULT: 377 | display_angles(result, sample_count); 378 | current_screen = SCR_MAIN_MENU; 379 | case SCR_LIVE: 380 | display_live_ranges(); 381 | current_screen = SCR_MAIN_MENU; 382 | case SCR_OSCOPE: 383 | display_oscilloscope(); 384 | current_screen = SCR_MAIN_MENU; 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/oscilloscope.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "range_test.h" 7 | #include "oscilloscope.h" 8 | #include "drawing.h" 9 | #include "text.h" 10 | #include "colors.h" 11 | #include "input.h" 12 | 13 | void display_oscilloscope() { 14 | int line_height = 11, 15 | sz_history = 230, 16 | x_offset = 24, 17 | y_offset1 = 70, 18 | y_offset2 = 170, 19 | lbl_x_offset = 282, 20 | lbl_y_offset1 = y_offset1 - (line_height / 2), 21 | lbl_y_offset2 = y_offset2 - (line_height / 2), 22 | count = 0; 23 | 24 | float zoom = 0.4; 25 | uint32_t c_blue = graphics_make_color(0, 192, 255, 255); 26 | uint32_t c_green = graphics_make_color(64, 255, 0, 255); 27 | struct Vec2 history[sz_history]; 28 | 29 | text_set_line_height(line_height); 30 | display_context_t ctx; 31 | 32 | for (;;) { 33 | while ((ctx = display_lock()) == 0) {} 34 | display_show(ctx); 35 | 36 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 37 | graphics_set_color(COLOR_FOREGROUND, 0); 38 | 39 | controller_scan(); 40 | struct controller_data cdata = get_keys_pressed(); 41 | 42 | struct Vec2 v = { cdata.c[0].x, cdata.c[0].y }; 43 | 44 | for (int i = count; i > 0; i--) { 45 | history[i] = history[i - 1]; 46 | draw_aa_line( 47 | ctx, 48 | x_offset + sz_history - i, 49 | y_offset1, 50 | x_offset + sz_history - i, 51 | y_offset1 + history[i].x * zoom, 52 | c_blue 53 | ); 54 | draw_aa_line( 55 | ctx, 56 | x_offset + sz_history - i, 57 | y_offset2, 58 | x_offset + sz_history - i, 59 | y_offset2 + (history[i].y * -1) * zoom, 60 | c_green 61 | ); 62 | } 63 | 64 | history[0] = v; 65 | draw_aa_line( 66 | ctx, 67 | x_offset + sz_history, 68 | y_offset1, 69 | x_offset + sz_history, 70 | y_offset1 + v.x * zoom, 71 | c_blue 72 | ); 73 | draw_aa_line( 74 | ctx, 75 | x_offset + sz_history, 76 | y_offset2, 77 | x_offset + sz_history, 78 | y_offset2 + (v.y * -1) * zoom, 79 | c_green 80 | ); 81 | 82 | if (count < sz_history - 1) { 83 | count++; 84 | } 85 | 86 | char buf[128]; 87 | 88 | text_set_font(FONT_BOLD); 89 | snprintf(buf, sizeof(buf), "%3d", v.x); 90 | text_draw(ctx, lbl_x_offset, lbl_y_offset1, buf, ALIGN_RIGHT); 91 | 92 | snprintf(buf, sizeof(buf), "%3d", v.y); 93 | text_draw(ctx, lbl_x_offset, lbl_y_offset2, buf, ALIGN_RIGHT); 94 | 95 | text_set_font(FONT_MEDIUM); 96 | snprintf(buf, sizeof(buf), "x"); 97 | text_draw(ctx, lbl_x_offset + 8, lbl_y_offset1, buf, ALIGN_LEFT); 98 | 99 | snprintf(buf, sizeof(buf), "y"); 100 | text_draw(ctx, lbl_x_offset + 8, lbl_y_offset2, buf, ALIGN_LEFT); 101 | 102 | 103 | snprintf(buf, sizeof(buf), "Oscilloscope display"); 104 | text_draw(ctx, 160, 15, buf, ALIGN_CENTER); 105 | 106 | text_set_font(FONT_MEDIUM); 107 | graphics_set_color(graphics_make_color(128, 128, 128, 255), 0); 108 | text_draw(ctx, 320 - 16, 213, REPO_URL, ALIGN_RIGHT); 109 | 110 | if (cdata.c[0].start) { 111 | break; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/oscilloscope.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void display_oscilloscope(); 4 | 5 | -------------------------------------------------------------------------------- /src/range_live.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "range_test.h" 7 | #include "range_live.h" 8 | #include "text.h" 9 | #include "colors.h" 10 | #include "input.h" 11 | 12 | struct StickAngles live_compare_oem = 13 | { 14 | .values = { 15 | 0, 85, 16 | 70, 70, 17 | 85, 0 , 18 | 70,-70, 19 | 0, -85, 20 | -70,-70, 21 | -85, 0 , 22 | -70, 70 23 | } 24 | }; 25 | 26 | struct StickAngles live_compare_hori = 27 | { 28 | .values = { 29 | 0, 100, 30 | 75, 75, 31 | 100,0, 32 | 75,-75, 33 | 0, -100, 34 | -75,-75, 35 | -100,0, 36 | -75, 75 37 | } 38 | }; 39 | 40 | struct StickAngles *live_comparisons[] = { 41 | NULL, 42 | &live_compare_oem, 43 | &live_compare_hori, 44 | }; 45 | 46 | char *get_title_str(int current_comparison) { 47 | switch (current_comparison) { 48 | case 1: 49 | return "Live, ideal OEM overlay"; 50 | case 2: 51 | return "Live, ideal Horipad Mini overlay"; 52 | } 53 | return "Live range display"; 54 | } 55 | 56 | void display_live_ranges() { 57 | int count = 0, 58 | line_height = 11, 59 | show_history = 1, 60 | sz_history = 1024, 61 | current_comparison = 1, 62 | comparison_count = sizeof(live_comparisons) / sizeof(0), 63 | zoomout = 0; 64 | float zoomout_factor = 1; 65 | text_set_line_height(line_height); 66 | display_context_t ctx; 67 | 68 | int f = dfs_open("/gfx/point.sprite"); 69 | int size = dfs_size(f); 70 | char * title_str = get_title_str(current_comparison); 71 | sprite_t *point = malloc(size); 72 | dfs_read(point, size, 1, f); 73 | dfs_close(f); 74 | 75 | struct Vec2 history[sz_history]; 76 | 77 | uint32_t comparison_color = graphics_make_color(64, 255, 0, 255), 78 | history_color = graphics_make_color(0, 192, 255, 255); 79 | 80 | for (;;) { 81 | while ((ctx = display_lock()) == 0) {} 82 | display_show(ctx); 83 | 84 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 85 | graphics_set_color(COLOR_FOREGROUND, 0); 86 | 87 | controller_scan(); 88 | struct controller_data cdata = get_keys_pressed(); 89 | char buf[128]; 90 | 91 | struct Vec2 v = { cdata.c[0].x, cdata.c[0].y }; 92 | 93 | snprintf(buf, sizeof(buf), "x\ny"); 94 | text_set_font(FONT_MEDIUM); 95 | text_draw(ctx, 290, 120 - line_height, buf, ALIGN_LEFT); 96 | 97 | text_set_font(FONT_BOLD); 98 | snprintf(buf, sizeof(buf), "%3d\n%3d", v.x, v.y); 99 | text_draw(ctx, 282, 120 - line_height, buf, ALIGN_RIGHT); 100 | 101 | draw_center_cross(ctx, 160); 102 | if (current_comparison > 0) { 103 | draw_stick_angles( 104 | ctx, 105 | *live_comparisons[current_comparison], 106 | comparison_color, 107 | zoomout, 108 | 160 109 | ); 110 | } 111 | 112 | if (show_history == 1) { 113 | int history_update = 0; 114 | if (v.x != history[0].x || v.y != history[0].y) { 115 | history_update = 1; 116 | if (count < sz_history - 1) { 117 | count++; 118 | } 119 | 120 | history[0] = v; 121 | } 122 | 123 | for (int i = count; i > 0; i--) { 124 | if (history_update == 1) history[i] = history[i - 1]; 125 | int x = smax(0, smin(320, (history[i].x * zoomout_factor) + 160)); 126 | int y = smax(0, smin(240, ((history[i].y * zoomout_factor) * -1) + 120)); 127 | graphics_draw_pixel(ctx, x, y, history_color); 128 | } 129 | } 130 | 131 | int x = smax(0, smin(320, (v.x * zoomout_factor) + 158)); 132 | int y = smax(0, smin(240, ((v.y * zoomout_factor) * -1) + 118)); 133 | graphics_draw_sprite(ctx, x, y, point); 134 | 135 | if (cdata.c[0].start) { 136 | break; 137 | } 138 | 139 | cdata = get_keys_down_filtered(); 140 | if (cdata.c[0].A) { 141 | show_history ^= 1; 142 | } 143 | 144 | if (cdata.c[0].B) { 145 | count = 0; 146 | } 147 | 148 | if (cdata.c[0].Z) { 149 | zoomout ^= 1; 150 | zoomout_factor = (zoomout == 0) ? 1 : 0.75; 151 | } 152 | 153 | if (cdata.c[0].left || cdata.c[0].L) { 154 | current_comparison--; 155 | if (current_comparison < 0) current_comparison += comparison_count; 156 | title_str = get_title_str(current_comparison); 157 | } 158 | 159 | if (cdata.c[0].right || cdata.c[0].R) { 160 | current_comparison = (current_comparison + 1) % comparison_count; 161 | title_str = get_title_str(current_comparison); 162 | } 163 | 164 | text_set_font(FONT_MEDIUM); 165 | snprintf(buf, sizeof(buf), "%s", title_str); 166 | text_draw(ctx, 160, 15, buf, ALIGN_CENTER); 167 | 168 | if (zoomout) { 169 | text_draw(ctx, 16, 213, "75\% scale", ALIGN_LEFT); 170 | } 171 | 172 | text_set_font(FONT_MEDIUM); 173 | graphics_set_color(graphics_make_color(128, 128, 128, 255), 0); 174 | text_draw(ctx, 320 - 16, 213, REPO_URL, ALIGN_RIGHT); 175 | } 176 | 177 | free(point); 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/range_live.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void display_live_ranges(); 4 | -------------------------------------------------------------------------------- /src/range_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "range_test.h" 7 | #include "colors.h" 8 | #include "drawing.h" 9 | #include "text.h" 10 | #include "util.h" 11 | #include "input.h" 12 | 13 | struct StickAngles perfect_n64 = 14 | { 15 | .values = { 16 | 0, 85, 17 | 70, 70, 18 | 85, 0 , 19 | 70,-70, 20 | 0, -85, 21 | -70,-70, 22 | -85, 0 , 23 | -70, 70 24 | } 25 | }; 26 | 27 | struct StickAngles perfect_hori = 28 | { 29 | .values = { 30 | 0, 100, 31 | 75, 75, 32 | 100,0, 33 | 75,-75, 34 | 0, -100, 35 | -75,-75, 36 | -100,0, 37 | -75, 75 38 | } 39 | }; 40 | 41 | enum Comparison 42 | { 43 | COMP_NONE, 44 | COMP_N64, 45 | COMP_HORI, 46 | 47 | COMP_MAX, 48 | }; 49 | 50 | struct StickAngles *comparisons[] = 51 | { 52 | NULL, 53 | &perfect_n64, 54 | &perfect_hori, 55 | }; 56 | 57 | const char *comparison_names[] = 58 | { 59 | " benchmark result", 60 | ", ideal N64 OEM comparison", 61 | ", ideal Horipad Mini comparison", 62 | }; 63 | 64 | const char *example_names[] = 65 | { 66 | "Ideal N64 OEM example", 67 | "Ideal Horipad Mini example", 68 | }; 69 | 70 | void draw_stick_angles(display_context_t ctx, struct StickAngles a, uint32_t color, int zoomout, int x) 71 | { 72 | if (zoomout) { 73 | for (int i = 0; i < 16; i++) { 74 | a.values[i] = (a.values[i] * 3) / 4; 75 | } 76 | } 77 | 78 | struct Vec2 *v = (struct Vec2*)&a; 79 | 80 | for (int i = 0; i < 8; i++) { 81 | int j = (i + 1) % 8; 82 | draw_aa_line( 83 | ctx, 84 | x + v[i].x, 85 | 120 - v[i].y, 86 | x + v[j].x, 87 | 120 - v[j].y, 88 | color); 89 | } 90 | } 91 | 92 | void draw_center_cross(display_context_t ctx, int x_origin) 93 | { 94 | int x, y, offset; 95 | y = 120; 96 | offset = x_origin - 120; 97 | for (x = offset; x < 240+offset; x++) { 98 | int i = smin(240 - abs(240 - (x-offset)*2), 120); 99 | graphics_draw_pixel_trans(ctx, x, y, graphics_make_color(255, 255, 255, i)); 100 | } 101 | 102 | x = x_origin; 103 | for (y = 0; y < 240; y++) { 104 | int i = smin(240 - abs(240 - y * 2), 120); 105 | graphics_draw_pixel_trans(ctx, x, y, graphics_make_color(255, 255, 255, i)); 106 | } 107 | } 108 | 109 | uint32_t get_range_color_cardinal(int a) 110 | { 111 | if (a >= 80) { 112 | return graphics_make_color(0, 255, 64, 255); 113 | } else if (a >= 75) { 114 | return graphics_make_color(192, 255, 0, 255); 115 | } else if (a >= 70) { 116 | return graphics_make_color(255, 128, 64, 255); 117 | } else { 118 | return graphics_make_color(255, 64, 0, 255); 119 | } 120 | } 121 | 122 | uint32_t get_range_color_diagonal(int x, int y) 123 | { 124 | float euclidean = sqrtf(x*x + y*y); 125 | 126 | return get_range_color_cardinal(euclidean / 1.125); 127 | } 128 | 129 | uint32_t get_angle_color(float angle) 130 | { 131 | 132 | float diff = fabsf(45.0f - angle); 133 | 134 | if (diff < 1) { 135 | return graphics_make_color(0, 255, 64, 255); 136 | } else if (diff < 3) { 137 | return graphics_make_color(192, 255, 0, 255); 138 | } else if (diff < 5) { 139 | return graphics_make_color(255, 128, 64, 255); 140 | } else { 141 | return graphics_make_color(255, 64, 0, 255); 142 | } 143 | } 144 | 145 | void print_stick_angles(display_context_t ctx, struct StickAngles a) 146 | { 147 | char buf[1024]; 148 | snprintf(buf, sizeof(buf), 149 | "up \n" 150 | "down \n" 151 | "left \n" 152 | "right\n\n" 153 | "UR\n\n\n" 154 | "UL\n\n\n" 155 | "DR\n\n\n" 156 | "DL" 157 | ); 158 | 159 | int y = 15; 160 | 161 | graphics_set_color(COLOR_FOREGROUND, 0); 162 | 163 | text_set_font(FONT_MEDIUM); 164 | text_draw(ctx, 270, y, buf, ALIGN_LEFT); 165 | 166 | text_set_font(FONT_BOLD); 167 | int cardinals[] = {a.u.y, -a.d.y, -a.l.x, a.r.x}; 168 | 169 | for (int i = 0; i < 4; i++) { 170 | snprintf(buf, sizeof(buf), "%3d", cardinals[i]); 171 | uint32_t c = get_range_color_cardinal(cardinals[i]); 172 | graphics_set_color(c, 0); 173 | text_draw(ctx, 263, y, buf, ALIGN_RIGHT); 174 | y += 10; 175 | } 176 | 177 | int diagonals[] = { 178 | a.ur.x, a.ur.y, 179 | -a.ul.x, a.ul.y, 180 | a.dr.x, -a.dr.y, 181 | -a.dl.x, -a.dl.y, 182 | }; 183 | 184 | y += 10; 185 | 186 | for (int i = 0; i < 8; i += 2) { 187 | snprintf(buf, sizeof(buf), "%3d\n%3d", diagonals[i], diagonals[i+1]); 188 | uint32_t c = get_range_color_diagonal(smax(0, diagonals[i]), smax(0, diagonals[i+1])); 189 | graphics_set_color(c, 0); 190 | text_draw(ctx, 263, y, buf, ALIGN_RIGHT); 191 | y += 30; 192 | } 193 | 194 | float angles[] = { 195 | get_angle(diagonals[0], diagonals[1]), 196 | get_angle(diagonals[2], diagonals[3]), 197 | get_angle(diagonals[4], diagonals[5]), 198 | get_angle(diagonals[6], diagonals[7]), 199 | }; 200 | 201 | y = 15 + 60; 202 | text_set_font(FONT_MEDIUM); 203 | 204 | for (int i = 0; i < 4; i++) { 205 | snprintf(buf, sizeof(buf), "%2.1f" SYMBOL_DEGREES, angles[i]); 206 | uint32_t c = get_angle_color(angles[i]); 207 | graphics_set_color(c, 0); 208 | text_draw(ctx, 270, y, buf, ALIGN_LEFT); 209 | y += 30; 210 | } 211 | } 212 | 213 | void test_angles(struct StickAngles *a, int testnum) 214 | { 215 | const int reset_cmd = 0xFF; 216 | 217 | static const char *angles[] = 218 | { 219 | "Neutral", 220 | "Up", 221 | "Up-Right", 222 | "Right", 223 | "Down-Right", 224 | "Down", 225 | "Down-Left", 226 | "Left", 227 | "Up-Left", 228 | }; 229 | 230 | static const char *gfx[] = 231 | { 232 | "/gfx/stick_neutral.sprite", 233 | "/gfx/stick_0.sprite", 234 | "/gfx/stick_1.sprite", 235 | "/gfx/stick_2.sprite", 236 | "/gfx/stick_3.sprite", 237 | "/gfx/stick_4.sprite", 238 | "/gfx/stick_5.sprite", 239 | "/gfx/stick_6.sprite", 240 | "/gfx/stick_7.sprite", 241 | }; 242 | 243 | struct Vec2 *v = (struct Vec2*)a; 244 | 245 | graphics_set_color(COLOR_FOREGROUND, 0); 246 | text_set_line_height(11); 247 | 248 | for (int i = 0; i < 9; i++) { 249 | int f = dfs_open(gfx[i]); 250 | int size = dfs_size(f); 251 | sprite_t *stick = malloc(size); 252 | dfs_read(stick, size, 1, f); 253 | dfs_close(f); 254 | 255 | display_context_t ctx; 256 | while ((ctx = display_lock()) == 0) {} 257 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 258 | graphics_draw_sprite(ctx, (320-128)/2, (240-128)/2, stick); 259 | 260 | char buf[128]; 261 | snprintf(buf, sizeof(buf), "Test %d\nHold %s and press A", testnum, angles[i]); 262 | text_set_font(FONT_BOLD); 263 | text_draw(ctx, 320/2, 24, buf, ALIGN_CENTER); 264 | 265 | display_show(ctx); 266 | 267 | for (;;) { 268 | controller_scan(); 269 | struct controller_data cdata = get_keys_down_filtered(); 270 | if (cdata.c[0].A) { 271 | if (i > 0 ) { 272 | cdata = get_keys_pressed(); 273 | v[i-1].x = cdata.c[0].x; 274 | v[i-1].y = cdata.c[0].y; 275 | } else { 276 | // raphnetraw needs some bytes for input or won't work, 277 | // dunno about real console 278 | uint8_t data[4]; 279 | execute_raw_command(0, reset_cmd, 0, 4, NULL, data); 280 | } 281 | break; 282 | } 283 | } 284 | 285 | free(stick); 286 | } 287 | } 288 | 289 | struct StickAngles find_median(struct StickAngles a[], int n) 290 | { 291 | struct StickAngles median; 292 | 293 | for (int i = 0; i < 16; i++) { 294 | int sorted[n]; 295 | 296 | for (int j = 0; j < n; j++) { 297 | sorted[j] = a[j].values[i]; 298 | } 299 | 300 | for (int j = 0; j < n; j++) { 301 | int lowest_idx = j; 302 | 303 | for (int k = j; k < n; k++) { 304 | if (sorted[k] < sorted[lowest_idx]) { 305 | lowest_idx = k; 306 | } 307 | } 308 | 309 | int tmp = sorted[j]; 310 | sorted[j] = sorted[lowest_idx]; 311 | sorted[lowest_idx] = tmp; 312 | } 313 | 314 | if (n % 2) { 315 | median.values[i] = sorted[(n-1)/2]; 316 | } else { 317 | median.values[i] = (sorted[n/2-1] + sorted[n/2]) / 2; 318 | } 319 | } 320 | 321 | return median; 322 | } 323 | 324 | float find_standard_deviation(struct StickAngles a[], int n) 325 | { 326 | if (n < 2) return -1; 327 | 328 | float values[16*n]; 329 | 330 | for (int i = 0; i < 16; i++) { 331 | float mean = 0; 332 | float *p = &values[n*i]; 333 | 334 | for (int j = 0; j < n; j++) { 335 | p[j] = a[j].values[i]; 336 | mean += p[j]; 337 | } 338 | 339 | mean = mean / (float)n; 340 | 341 | // the values need to be normalized so each notch's axis 342 | // becomes comparable to each other 343 | for (int j = 0; j < n; j++) { 344 | p[j] -= mean; 345 | } 346 | } 347 | 348 | float variance = 0; 349 | 350 | for (int i = 0; i < 16 * n; i++) { 351 | float v = values[i]; 352 | variance += v * v; 353 | } 354 | variance = variance / (float)(16*n - 1); 355 | 356 | return sqrtf(variance); 357 | } 358 | 359 | int should_enable_zoomout(struct StickAngles a[], int n) { 360 | for (int i = 0; i < n; i++) { 361 | for (int j = 0; j < 16; j++) { 362 | if (abs(a[i].values[j]) > 108) { 363 | return 1; 364 | } 365 | } 366 | } 367 | 368 | return 0; 369 | } 370 | 371 | void display_angles(struct StickAngles a[], int sample_count) 372 | { 373 | enum Comparison current_comparison = COMP_NONE; 374 | display_context_t ctx; 375 | int current_measurement = 0; 376 | int current_example = 0; 377 | 378 | uint32_t c_blue = graphics_make_color(0, 192, 255, 255); 379 | uint32_t c_green = graphics_make_color(64, 255, 0, 255); 380 | uint32_t c_gray = graphics_make_color(64, 64, 64, 255); 381 | uint32_t c_magenta = graphics_make_color(255, 0, 192, 255); 382 | uint32_t c_current = c_blue; 383 | 384 | struct StickAngles median = find_median(a, sample_count); 385 | int zoomout = should_enable_zoomout(a, sample_count); 386 | int x_origin = 120; 387 | 388 | text_set_line_height(10); 389 | for (;;) { 390 | while ((ctx = display_lock()) == 0) {} 391 | 392 | graphics_fill_screen(ctx, COLOR_BACKGROUND); 393 | 394 | draw_center_cross(ctx, x_origin); 395 | for (int i = 0; i < sample_count; i++) { 396 | draw_stick_angles(ctx, a[i], c_gray, zoomout, x_origin); 397 | } 398 | if (comparisons[current_comparison]) { 399 | draw_stick_angles(ctx, *comparisons[current_comparison], c_green, zoomout, x_origin); 400 | } 401 | 402 | struct StickAngles *current; 403 | if (current_measurement > 0) { 404 | current = &a[current_measurement - 1]; 405 | } else { 406 | current = &median; 407 | } 408 | if (current_example > 0) { 409 | current = comparisons[current_example]; 410 | c_current = c_magenta; 411 | } else { 412 | c_current = c_blue; 413 | } 414 | 415 | draw_stick_angles(ctx, *current, c_current, zoomout, x_origin); 416 | print_stick_angles(ctx, *current); 417 | 418 | graphics_set_color(COLOR_FOREGROUND, 0); 419 | int y = 15 + 10*17; 420 | 421 | char buf[128]; 422 | snprintf(buf, sizeof(buf), "%d", sample_count); 423 | text_set_font(FONT_BOLD); 424 | text_draw(ctx, 263, y, buf, ALIGN_RIGHT); 425 | 426 | text_set_font(FONT_MEDIUM); 427 | if (sample_count == 1) { 428 | text_draw(ctx, 270, y, "test", ALIGN_LEFT); 429 | } else { 430 | text_draw(ctx, 270, y, "tests", ALIGN_LEFT); 431 | } 432 | 433 | y += 10; 434 | if (sample_count > 1) { 435 | float sd = find_standard_deviation(a, sample_count); 436 | snprintf(buf, sizeof(buf), "%.2f", sd); 437 | 438 | text_set_font(FONT_BOLD); 439 | text_draw(ctx, 263, y, buf, ALIGN_RIGHT); 440 | 441 | text_set_font(FONT_MEDIUM); 442 | text_draw(ctx, 270, y, "std dev", ALIGN_LEFT); 443 | } 444 | 445 | if (sample_count == 1) { 446 | current_measurement = 1; 447 | } 448 | if (current_example == 0) { 449 | if (current_measurement > 0) { 450 | snprintf(buf, sizeof(buf), "Test %d%s", 451 | current_measurement, comparison_names[current_comparison]); 452 | } else { 453 | snprintf(buf, sizeof(buf), "Median%s", 454 | comparison_names[current_comparison]); 455 | } 456 | } else { 457 | if (current_comparison > 0) { 458 | snprintf(buf, sizeof(buf), "Example%s", 459 | comparison_names[current_comparison]); 460 | } else { 461 | snprintf(buf, sizeof(buf), "%s", 462 | example_names[current_example-1]); 463 | } 464 | } 465 | 466 | text_draw(ctx, 120, 15, buf, ALIGN_CENTER); 467 | 468 | if (zoomout) { 469 | text_draw(ctx, 16, 213, "75\% scale", ALIGN_LEFT); 470 | } 471 | 472 | text_set_font(FONT_MEDIUM); 473 | graphics_set_color(graphics_make_color(128, 128, 128, 255), 0); 474 | text_draw(ctx, 320 - 16, 213, REPO_URL, ALIGN_RIGHT); 475 | 476 | display_show(ctx); 477 | 478 | controller_scan(); 479 | struct controller_data cdata = get_keys_down_filtered(); 480 | if (cdata.c[0].start) { 481 | return; 482 | } 483 | 484 | if (cdata.c[0].L) { 485 | if (current_comparison == 0) { 486 | current_comparison = COMP_MAX - 1; 487 | } else { 488 | current_comparison--; 489 | } 490 | } else if (cdata.c[0].R) { 491 | current_comparison++; 492 | if (current_comparison >= COMP_MAX) { 493 | current_comparison = 0; 494 | } 495 | } 496 | 497 | if (cdata.c[0].left) { 498 | if (current_example == 0) { 499 | current_example = COMP_MAX - 1; 500 | } else { 501 | current_example--; 502 | } 503 | } else if (cdata.c[0].right) { 504 | current_example++; 505 | if (current_example >= COMP_MAX) { 506 | current_example = 0; 507 | } 508 | } 509 | 510 | if (cdata.c[0].up) { 511 | if (current_measurement <= 0) { 512 | current_measurement = sample_count; 513 | } else { 514 | current_measurement--; 515 | } 516 | } else if (cdata.c[0].down) { 517 | current_measurement++; 518 | if (current_measurement > sample_count) { 519 | current_measurement = 0; 520 | } 521 | } 522 | 523 | if (cdata.c[0].Z) { 524 | zoomout ^= 1; 525 | } 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /src/range_test.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "util.h" 4 | 5 | struct StickAngles 6 | { 7 | union { 8 | struct { 9 | struct Vec2 u; 10 | struct Vec2 ur; 11 | struct Vec2 r; 12 | struct Vec2 dr; 13 | struct Vec2 d; 14 | struct Vec2 dl; 15 | struct Vec2 l; 16 | struct Vec2 ul; 17 | }; 18 | int values[16]; 19 | }; 20 | }; 21 | 22 | void test_angles(struct StickAngles *a, int testnum); 23 | void display_angles(struct StickAngles a[], int sample_count); 24 | void draw_center_cross(display_context_t ctx, int x_origin); 25 | void draw_stick_angles(display_context_t ctx, struct StickAngles a, uint32_t color, int zoomout, int x); 26 | -------------------------------------------------------------------------------- /src/text.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "text.h" 4 | 5 | struct FontData 6 | { 7 | sprite_t *sprite; 8 | uint8_t char_widths[128]; 9 | }; 10 | 11 | struct FontData fonts[2]; 12 | enum Font current_font = 0; 13 | int line_height = 10; 14 | 15 | void load_font(struct FontData *fontdata, const char sprite_path[], const char charwidth_path[]) 16 | { 17 | int f = dfs_open(sprite_path); 18 | int size = dfs_size(f); 19 | sprite_t *sprite = malloc(size); 20 | dfs_read(sprite, size, 1, f); 21 | fontdata->sprite = sprite; 22 | dfs_close(f); 23 | 24 | f = dfs_open(charwidth_path); 25 | dfs_read(fontdata->char_widths, sizeof(fontdata->char_widths), 1, f); 26 | dfs_close(f); 27 | } 28 | 29 | void text_init() 30 | { 31 | load_font(&fonts[FONT_MEDIUM], "font/medium.sprite", "font/medium_charwidths.bin"); 32 | load_font(&fonts[FONT_BOLD], "font/bold.sprite", "font/bold_charwidths.bin"); 33 | text_set_font(FONT_MEDIUM); 34 | text_set_line_height(11); 35 | } 36 | 37 | void text_set_font(enum Font f) 38 | { 39 | current_font = f; 40 | graphics_set_font_sprite(fonts[f].sprite); 41 | } 42 | 43 | void text_set_line_height(int h) 44 | { 45 | line_height = h; 46 | } 47 | 48 | int text_get_line_width(const char s[]) 49 | { 50 | int w = 0; 51 | for (; *s != 0 && *s != '\n'; s++) { 52 | w += fonts[current_font].char_widths[(int)*s]; 53 | } 54 | 55 | return w; 56 | } 57 | 58 | void text_draw(display_context_t ctx, int x, int y, const char s[], enum TextAlign align) 59 | { 60 | int x_init = x; 61 | 62 | reposition: 63 | int w = text_get_line_width(s); 64 | if (align == ALIGN_RIGHT) { 65 | x -= w; 66 | } 67 | if (align == ALIGN_CENTER) { 68 | x -= w / 2; 69 | } 70 | 71 | for (; *s != 0; s++) { 72 | switch (*s) 73 | { 74 | case '\n': 75 | x = x_init; 76 | y += line_height; 77 | s++; 78 | goto reposition; 79 | break; 80 | case ' ': 81 | x += fonts[current_font].char_widths[(int)*s]; 82 | break; 83 | default: 84 | graphics_draw_character(ctx, x, y, *s); 85 | x += fonts[current_font].char_widths[(int)*s]; 86 | break; 87 | } 88 | } 89 | } 90 | 91 | int text_get_max_chars_line(const char s[], int width) 92 | { 93 | const char *s_init = s; 94 | const char *s_prev = s; 95 | int w = 0; 96 | for (; *s != 0; s++) { 97 | if (w > width) { 98 | if (s_prev == s_init) { 99 | return s - s_init; 100 | } if (s == s_init) { 101 | return 1; 102 | } else { 103 | return s_prev - s_init; 104 | } 105 | } 106 | 107 | if (*s == ' ') { 108 | s_prev = s; 109 | } 110 | 111 | if (*s == '\n') { 112 | return s - s_init + 1; 113 | } 114 | 115 | w += fonts[current_font].char_widths[(int)*s]; 116 | } 117 | 118 | return s - s_init + 1; 119 | } 120 | 121 | void text_draw_wordwrap(display_context_t ctx, int x, int y, int w, const char s[]) 122 | { 123 | int x_init = x; 124 | 125 | while (*s != 0) { 126 | while (*s == ' ') s++; 127 | 128 | for (int count = text_get_max_chars_line(s, w); count > 0; count--) { 129 | switch (*s) 130 | { 131 | case 0: 132 | return; 133 | case '\n': 134 | break; 135 | case ' ': 136 | x += fonts[current_font].char_widths[(int)*s]; 137 | break; 138 | default: 139 | graphics_draw_character(ctx, x, y, *s); 140 | x += fonts[current_font].char_widths[(int)*s]; 141 | break; 142 | } 143 | s++; 144 | } 145 | 146 | x = x_init; 147 | y += line_height; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/text.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define SYMBOL_DEGREES "\x1f" 6 | 7 | enum Font 8 | { 9 | FONT_MEDIUM = 0, 10 | FONT_BOLD = 1, 11 | }; 12 | 13 | enum TextAlign 14 | { 15 | ALIGN_LEFT, 16 | ALIGN_RIGHT, 17 | ALIGN_CENTER, 18 | }; 19 | 20 | void text_init(); 21 | void text_set_font(enum Font f); 22 | void text_set_line_height(int h); 23 | int text_get_line_width(const char s[]); 24 | void text_draw(display_context_t ctx, int x, int y, const char s[], enum TextAlign align); 25 | void text_draw_wordwrap(display_context_t ctx, int x, int y, int w, const char s[]); 26 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Vec2 6 | { 7 | int x; 8 | int y; 9 | }; 10 | 11 | static inline int smin(int a, int b) 12 | { 13 | return a > b ? b : a; 14 | } 15 | 16 | static inline int smax(int a, int b) 17 | { 18 | return a > b ? a : b; 19 | } 20 | 21 | static inline float get_angle(float x, float y) 22 | { 23 | return atan2f(x, y) * 180 / 3.14159; 24 | } 25 | --------------------------------------------------------------------------------