├── .github ├── FUNDING.yml ├── pull_request_template.md └── workflows │ └── autobuild.yml ├── romfs ├── font.bmp ├── file-dark.bmp ├── file-light.bmp ├── folder-dark.bmp └── folder-light.bmp ├── .gitignore ├── com.hydra.rokuyon.desktop ├── src ├── desktop │ ├── main.cpp │ ├── ry_app.h │ ├── save_dialog.h │ ├── ry_canvas.h │ ├── ry_frame.h │ ├── input_dialog.h │ ├── ry_app.cpp │ ├── save_dialog.cpp │ ├── ry_canvas.cpp │ ├── ry_frame.cpp │ └── input_dialog.cpp ├── si.h ├── rdp.h ├── pi.h ├── rsp.h ├── rsp_cp0.h ├── ai.h ├── cpu.h ├── rsp_cp2.h ├── pif.h ├── mi.h ├── settings.h ├── vi.h ├── cpu_cp0.h ├── memory.h ├── cpu_cp1.h ├── log.h ├── core.h ├── mi.cpp ├── settings.cpp ├── switch │ ├── switch_ui.h │ └── main.cpp ├── si.cpp ├── pi.cpp ├── rsp_cp0.cpp ├── vi.cpp ├── ai.cpp ├── core.cpp ├── pif.cpp ├── cpu_cp0.cpp └── memory.cpp ├── com.hydra.rokuyon.yml ├── Info.plist ├── mac-bundle.sh ├── Makefile ├── README.md └── Makefile.switch /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: paypal.me/Hydr8gon 2 | -------------------------------------------------------------------------------- /romfs/font.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hydr8gon/rokuyon/HEAD/romfs/font.bmp -------------------------------------------------------------------------------- /romfs/file-dark.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hydr8gon/rokuyon/HEAD/romfs/file-dark.bmp -------------------------------------------------------------------------------- /romfs/file-light.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hydr8gon/rokuyon/HEAD/romfs/file-light.bmp -------------------------------------------------------------------------------- /romfs/folder-dark.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hydr8gon/rokuyon/HEAD/romfs/folder-dark.bmp -------------------------------------------------------------------------------- /romfs/folder-light.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hydr8gon/rokuyon/HEAD/romfs/folder-light.bmp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /build-switch 3 | /rokuyon 4 | /rokuyon.elf 5 | /rokuyon.nacp 6 | /rokuyon.nro 7 | pif_rom.bin 8 | rokuyon.ini 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Pull requests are not accepted for this project; see the contributing section of the readme for more details. 2 | -------------------------------------------------------------------------------- /com.hydra.rokuyon.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=rokuyon 4 | Comment=An experimental N64 emulator 5 | GenericName=Nintendo 64 Emulator 6 | Icon=com.hydra.rokuyon 7 | Exec=rokuyon %U 8 | StartupNotify=true 9 | Terminal=false 10 | MimeType=application/x-n64-rom 11 | Categories=Game;Emulator 12 | Keywords=emulator;nintendo;64;n64 13 | -------------------------------------------------------------------------------- /src/desktop/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "ry_app.h" 21 | 22 | // Let wxWidgets handle the main function 23 | wxIMPLEMENT_APP(ryApp); 24 | -------------------------------------------------------------------------------- /src/si.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef SI_H 21 | #define SI_H 22 | 23 | #include 24 | 25 | namespace SI 26 | { 27 | void reset(); 28 | uint32_t read(uint32_t address); 29 | void write(uint32_t address, uint32_t value); 30 | } 31 | 32 | #endif // SI_H 33 | -------------------------------------------------------------------------------- /src/rdp.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef RDP_H 21 | #define RDP_H 22 | 23 | #include 24 | 25 | namespace RDP 26 | { 27 | void reset(); 28 | uint32_t read(int index); 29 | void write(int index, uint32_t value); 30 | void finishThread(); 31 | } 32 | 33 | #endif // RDP_H 34 | -------------------------------------------------------------------------------- /src/pi.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef PI_H 21 | #define PI_H 22 | 23 | #include 24 | #include 25 | 26 | namespace PI 27 | { 28 | void reset(); 29 | uint32_t read(uint32_t address); 30 | void write(uint32_t address, uint32_t value); 31 | } 32 | 33 | #endif // PI_H 34 | -------------------------------------------------------------------------------- /src/rsp.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef RSP_H 21 | #define RSP_H 22 | 23 | #include 24 | 25 | namespace RSP 26 | { 27 | void reset(); 28 | uint32_t readPC(); 29 | void writePC(uint32_t value); 30 | void setState(bool halted); 31 | void runOpcode(); 32 | } 33 | 34 | #endif // RSP_H 35 | -------------------------------------------------------------------------------- /src/rsp_cp0.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef RSP_CP0_H 21 | #define RSP_CP0_H 22 | 23 | #include 24 | 25 | namespace RSP_CP0 26 | { 27 | void reset(); 28 | uint32_t read(int index); 29 | void write(int index, uint32_t value); 30 | void triggerBreak(); 31 | } 32 | 33 | #endif // RSP_CP0_H 34 | -------------------------------------------------------------------------------- /src/ai.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef AI_H 21 | #define AI_H 22 | 23 | #include 24 | 25 | namespace AI 26 | { 27 | void fillBuffer(uint32_t *out); 28 | 29 | void reset(); 30 | uint32_t read(uint32_t address); 31 | void write(uint32_t address, uint32_t value); 32 | } 33 | 34 | #endif // AI_H 35 | -------------------------------------------------------------------------------- /src/cpu.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef CPU_H 21 | #define CPU_H 22 | 23 | #include 24 | 25 | namespace CPU 26 | { 27 | extern uint64_t *registersW[32]; 28 | extern uint32_t programCounter; 29 | extern uint32_t nextOpcode; 30 | extern uint32_t delaySlot; 31 | 32 | void reset(); 33 | void runOpcode(); 34 | } 35 | 36 | #endif // CPU_H 37 | -------------------------------------------------------------------------------- /src/rsp_cp2.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef RSP_CP2_H 21 | #define RSP_CP2_H 22 | 23 | #include 24 | 25 | namespace RSP_CP2 26 | { 27 | extern void (*vecInstrs[])(uint32_t); 28 | 29 | void reset(); 30 | int16_t read(bool control, int index, int byte); 31 | void write(bool control, int index, int byte, int16_t value); 32 | } 33 | 34 | #endif // RSP_CP2_H 35 | -------------------------------------------------------------------------------- /src/pif.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef PIF_H 21 | #define PIF_H 22 | 23 | #include 24 | #include 25 | 26 | namespace PIF 27 | { 28 | extern uint8_t memory[0x800]; 29 | 30 | void reset(); 31 | void runCommand(); 32 | 33 | void pressKey(int key); 34 | void releaseKey(int key); 35 | void setStick(int x, int y); 36 | } 37 | 38 | #endif // PIF_H 39 | -------------------------------------------------------------------------------- /src/mi.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef MI_H 21 | #define MI_H 22 | 23 | #include 24 | 25 | namespace MI 26 | { 27 | extern uint32_t interrupt; 28 | extern uint32_t mask; 29 | 30 | void reset(); 31 | uint32_t read(uint32_t address); 32 | void write(uint32_t address, uint32_t value); 33 | 34 | void setInterrupt(int bit); 35 | void clearInterrupt(int bit); 36 | } 37 | 38 | #endif // MI_H 39 | -------------------------------------------------------------------------------- /src/settings.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef SETTINGS_H 21 | #define SETTINGS_H 22 | 23 | #include 24 | 25 | namespace Settings 26 | { 27 | void add(std::string name, void *value, bool isString); 28 | bool load(std::string filename = "rokuyon.ini"); 29 | bool save(); 30 | 31 | extern int fpsLimiter; 32 | extern int expansionPak; 33 | extern int threadedRdp; 34 | extern int texFilter; 35 | } 36 | 37 | #endif // SETTINGS_H 38 | -------------------------------------------------------------------------------- /src/vi.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef VI_H 21 | #define VI_H 22 | 23 | #include 24 | 25 | struct _Framebuffer 26 | { 27 | ~_Framebuffer() { delete[] data; } 28 | 29 | uint32_t *data; 30 | uint32_t width; 31 | uint32_t height; 32 | }; 33 | 34 | namespace VI 35 | { 36 | _Framebuffer *getFramebuffer(); 37 | 38 | void reset(); 39 | uint32_t read(uint32_t address); 40 | void write(uint32_t address, uint32_t value); 41 | } 42 | 43 | #endif // VI_H 44 | -------------------------------------------------------------------------------- /src/cpu_cp0.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef CPU_CP0_H 21 | #define CPU_CP0_H 22 | 23 | #include 24 | 25 | namespace CPU_CP0 26 | { 27 | extern void (*cp0Instrs[])(uint32_t); 28 | 29 | void reset(); 30 | int32_t read(int index); 31 | void write(int index, int32_t value); 32 | 33 | void resetCycles(); 34 | void checkInterrupts(); 35 | void exception(uint8_t type); 36 | void setTlbAddress(uint32_t address); 37 | bool cpUsable(uint8_t cp); 38 | } 39 | 40 | #endif // CPU_CP0_H 41 | -------------------------------------------------------------------------------- /src/memory.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef MEMORY_H 21 | #define MEMORY_H 22 | 23 | #include 24 | 25 | namespace Memory 26 | { 27 | void reset(); 28 | void getEntry(uint32_t index, uint32_t &entryLo0, uint32_t &entryLo1, uint32_t &entryHi, uint32_t &pageMask); 29 | void setEntry(uint32_t index, uint32_t entryLo0, uint32_t entryLo1, uint32_t entryHi, uint32_t pageMask); 30 | 31 | template T read(uint32_t address); 32 | template void write(uint32_t address, T value); 33 | } 34 | 35 | #endif // MEMORY_H 36 | -------------------------------------------------------------------------------- /src/cpu_cp1.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef CPU_CP1_H 21 | #define CPU_CP1_H 22 | 23 | #include 24 | 25 | enum CP1Type 26 | { 27 | CP1_32BIT = 0, 28 | CP1_64BIT, 29 | CP1_CTRL 30 | }; 31 | 32 | namespace CPU_CP1 33 | { 34 | extern void (*sglInstrs[])(uint32_t); 35 | extern void (*dblInstrs[])(uint32_t); 36 | extern void (*wrdInstrs[])(uint32_t); 37 | extern void (*lwdInstrs[])(uint32_t); 38 | 39 | void reset(); 40 | uint64_t read(CP1Type type, int index); 41 | void write(CP1Type type, int index, uint64_t value); 42 | void setRegMode(bool full); 43 | } 44 | 45 | #endif // CPU_CP1_H 46 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef LOG_H 21 | #define LOG_H 22 | 23 | #include 24 | 25 | // If enabled, print critical logs in red 26 | #if LOG_LEVEL > 0 27 | #define LOG_CRIT(...) printf("\x1b[31m" __VA_ARGS__) 28 | #else 29 | #define LOG_CRIT(...) (0) 30 | #endif 31 | 32 | // If enabled, print warning logs in yellow 33 | #if LOG_LEVEL > 1 34 | #define LOG_WARN(...) printf("\x1b[33m" __VA_ARGS__) 35 | #else 36 | #define LOG_WARN(...) (0) 37 | #endif 38 | 39 | // If enabled, print info logs normally 40 | #if LOG_LEVEL > 2 41 | #define LOG_INFO(...) printf("\x1b[0m" __VA_ARGS__) 42 | #else 43 | #define LOG_INFO(...) (0) 44 | #endif 45 | 46 | #endif // LOG_H 47 | -------------------------------------------------------------------------------- /src/core.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef CORE_H 21 | #define CORE_H 22 | 23 | #include 24 | #include 25 | 26 | namespace Core 27 | { 28 | extern bool running; 29 | extern bool cpuRunning; 30 | extern bool rspRunning; 31 | extern uint32_t globalCycles; 32 | extern int fps; 33 | 34 | extern uint8_t *rom; 35 | extern uint8_t *save; 36 | extern uint32_t romSize; 37 | extern uint32_t saveSize; 38 | 39 | bool bootRom(const std::string &path); 40 | void resizeSave(uint32_t newSize); 41 | void start(); 42 | void stop(); 43 | 44 | void countFrame(); 45 | void writeSave(uint32_t address, uint8_t value); 46 | void schedule(void (*function)(), uint32_t cycles); 47 | } 48 | 49 | #endif // CORE_H 50 | -------------------------------------------------------------------------------- /src/desktop/ry_app.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef RY_APP_H 21 | #define RY_APP_H 22 | 23 | #include 24 | 25 | #include "ry_frame.h" 26 | 27 | #define MAX_KEYS 20 28 | 29 | class ryApp: public wxApp 30 | { 31 | public: 32 | static int keyBinds[MAX_KEYS]; 33 | 34 | private: 35 | ryFrame *frame; 36 | wxTimer *timer; 37 | PaStream *stream; 38 | 39 | bool OnInit(); 40 | int OnExit(); 41 | 42 | void update(wxTimerEvent &event); 43 | 44 | static int audioCallback(const void *in, void *out, unsigned long count, 45 | const PaStreamCallbackTimeInfo *info, PaStreamCallbackFlags flags, void *data); 46 | 47 | wxDECLARE_EVENT_TABLE(); 48 | }; 49 | 50 | #endif // RY_APP_H 51 | -------------------------------------------------------------------------------- /src/desktop/save_dialog.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef SAVE_DIALOG_H 21 | #define SAVE_DIALOG_H 22 | 23 | #include 24 | #include 25 | 26 | class SaveDialog: public wxDialog 27 | { 28 | public: 29 | SaveDialog(std::string &lastPath); 30 | 31 | private: 32 | std::string &lastPath; 33 | uint32_t selection = 0; 34 | 35 | uint32_t selectToSize(uint32_t select); 36 | uint32_t sizeToSelect(uint32_t size); 37 | 38 | void select0(wxCommandEvent &event); 39 | void select1(wxCommandEvent &event); 40 | void select2(wxCommandEvent &event); 41 | void select3(wxCommandEvent &event); 42 | void select4(wxCommandEvent &event); 43 | void confirm(wxCommandEvent &event); 44 | 45 | wxDECLARE_EVENT_TABLE(); 46 | }; 47 | 48 | #endif // SAVE_DIALOG_H 49 | -------------------------------------------------------------------------------- /com.hydra.rokuyon.yml: -------------------------------------------------------------------------------- 1 | app-id: com.hydra.rokuyon 2 | runtime: org.freedesktop.Platform 3 | runtime-version: '21.08' 4 | sdk: org.freedesktop.Sdk 5 | command: rokuyon 6 | 7 | finish-args: 8 | - --device=all 9 | - --share=ipc 10 | - --socket=x11 11 | - --socket=pulseaudio 12 | - --filesystem=host 13 | 14 | modules: 15 | - name: wxwidgets 16 | buildsystem: cmake-ninja 17 | config-opts: 18 | - -DCMAKE_BUILD_TYPE=Release 19 | sources: 20 | - type: git 21 | url: https://github.com/wxWidgets/wxWidgets.git 22 | tag: v3.1.7 23 | modules: 24 | - name: glu 25 | config-opts: 26 | - --disable-static 27 | sources: 28 | - type: archive 29 | url: https://ftp.osuosl.org/pub/blfs/conglomeration/glu/glu-9.0.2.tar.xz 30 | sha256: 6e7280ff585c6a1d9dfcdf2fca489251634b3377bfc33c29e4002466a38d02d4 31 | cleanup: 32 | - /include 33 | - /lib/*.a 34 | - /lib/*.la 35 | - /lib/pkgconfig 36 | cleanup: 37 | - /bin 38 | - /include 39 | - /lib/wx/include 40 | 41 | - name: portaudio 42 | config-opts: 43 | - --disable-static 44 | - --without-oss 45 | - --without-jack 46 | sources: 47 | - type: git 48 | url: https://github.com/PortAudio/portaudio.git 49 | tag: v19.7.0 50 | cleanup: 51 | - /include 52 | - /lib/*.la 53 | - /lib/pkgconfig 54 | 55 | - name: rokuyon 56 | buildsystem: simple 57 | build-commands: 58 | - DESTDIR=/app make install 59 | sources: 60 | - type: git 61 | url: https://github.com/Hydr8gon/rokuyon.git 62 | branch: main 63 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | rokuyon 9 | CFBundleGetInfoString 10 | 11 | CFBundleIconFile 12 | rokuyon.icns 13 | CFBundleIdentifier 14 | com.hydra.rokuyon 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | rokuyon 19 | CFBundlePackageType 20 | APPL 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | CFBundleShortVersionString 26 | 1.0 27 | CSResourcesFileMapped 28 | 29 | NSHighResolutionCapable 30 | 31 | NSHumanReadableCopyright 32 | Licensed under GPLv3 33 | NSSupportsAutomaticGraphicsSwitching 34 | 35 | NSRequiresAquaSystemAppearance 36 | 37 | CFBundleDocumentTypes 38 | 39 | 40 | CFBundleTypeExtensions 41 | 42 | z64 43 | 44 | CFBundleTypeName 45 | Nintendo 64 ROM Image 46 | CFBundleTypeRole 47 | Viewer 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/desktop/ry_canvas.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef RY_CANVAS_H 21 | #define RY_CANVAS_H 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | class ryFrame; 28 | 29 | class ryCanvas: public wxGLCanvas 30 | { 31 | public: 32 | ryCanvas(ryFrame *frame); 33 | 34 | void finish(); 35 | 36 | private: 37 | ryFrame *frame; 38 | wxGLContext *context; 39 | 40 | int frameCount = 0; 41 | int swapInterval = 0; 42 | int refreshRate = 0; 43 | std::chrono::steady_clock::time_point lastRateTime; 44 | 45 | uint32_t width = 0; 46 | uint32_t height = 0; 47 | uint32_t x = 0; 48 | uint32_t y = 0; 49 | 50 | uint8_t sizeReset = 0; 51 | bool fullScreen = false; 52 | bool finished = false; 53 | 54 | void draw(wxPaintEvent &event); 55 | void resize(wxSizeEvent &event); 56 | void pressKey(wxKeyEvent &event); 57 | void releaseKey(wxKeyEvent &event); 58 | 59 | wxDECLARE_EVENT_TABLE(); 60 | }; 61 | 62 | #endif // RY_CANVAS_H 63 | -------------------------------------------------------------------------------- /mac-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | 6 | app=rokuyon.app 7 | contents=$app/Contents 8 | 9 | if [[ ! -f rokuyon ]]; then 10 | echo 'Error: rokuyon binary was not found.' 11 | echo 'Please run `make` to compile rokuyon before bundling.' 12 | exit 1 13 | fi 14 | 15 | if [[ -d "$app" ]]; then 16 | rm -rf "$app" 17 | fi 18 | 19 | install -dm755 "${contents}"/{MacOS,Resources,Frameworks} 20 | install -sm755 rokuyon "${contents}/MacOS/rokuyon" 21 | install -m644 Info.plist "$contents/Info.plist" 22 | 23 | # macOS does not have the -f flag for readlink 24 | abspath() { 25 | perl -MCwd -le 'print Cwd::abs_path shift' "$1" 26 | } 27 | 28 | # Recursively copy dependent libraries to the Frameworks directory 29 | # and fix their load paths 30 | fixup_libs() { 31 | local libs=($(otool -L "$1" | grep -vE "/System|/usr/lib|:$" | sed -E 's/'$'\t''(.*) \(.*$/\1/')) 32 | 33 | for lib in "${libs[@]}"; do 34 | # Dereference symlinks to get the actual .dylib as binaries' load 35 | # commands can contain paths to symlinked libraries. 36 | local abslib="$(abspath "$lib")" 37 | local base="$(basename "$abslib")" 38 | local install_path="$contents/Frameworks/$base" 39 | 40 | install_name_tool -change "$lib" "@rpath/$base" "$1" 41 | 42 | if [[ ! -f "$install_path" ]]; then 43 | install -m644 "$abslib" "$install_path" 44 | strip -Sx "$install_path" 45 | fixup_libs "$install_path" 46 | fi 47 | done 48 | } 49 | 50 | install_name_tool -add_rpath "@executable_path/../Frameworks" $contents/MacOS/rokuyon 51 | 52 | fixup_libs $contents/MacOS/rokuyon 53 | 54 | codesign --deep -s - rokuyon.app 55 | 56 | if [[ $1 == '--dmg' ]]; then 57 | mkdir build/dmg 58 | cp -a rokuyon.app build/dmg/ 59 | ln -s /Applications build/dmg/Applications 60 | hdiutil create -volname rokuyon -srcfolder build/dmg -ov -format UDBZ rokuyon.dmg 61 | rm -r build/dmg 62 | fi 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := rokuyon 2 | BUILD := build 3 | SRCS := src src/desktop 4 | ARGS := -O3 -flto -std=c++11 -DLOG_LEVEL=0 5 | LIBS := $(shell pkg-config --libs portaudio-2.0) 6 | INCS := $(shell pkg-config --cflags portaudio-2.0) 7 | 8 | APPNAME := rokuyon 9 | PKGNAME := com.hydra.rokuyon 10 | DESTDIR ?= /usr 11 | 12 | ifeq ($(OS),Windows_NT) 13 | ARGS += -static -DWINDOWS 14 | LIBS += $(shell wx-config-static --libs std,gl) -lole32 -lsetupapi -lwinmm 15 | INCS += $(shell wx-config-static --cxxflags std,gl) 16 | else 17 | LIBS += $(shell wx-config --libs std,gl) 18 | INCS += $(shell wx-config --cxxflags std,gl) 19 | ifeq ($(shell uname -s),Darwin) 20 | ARGS += -DMACOS 21 | LIBS += -headerpad_max_install_names 22 | else 23 | ARGS += -no-pie 24 | LIBS += -lGL 25 | endif 26 | endif 27 | 28 | CPPFILES := $(foreach dir,$(SRCS),$(wildcard $(dir)/*.cpp)) 29 | HFILES := $(foreach dir,$(SRCS),$(wildcard $(dir)/*.h)) 30 | OFILES := $(patsubst %.cpp,$(BUILD)/%.o,$(CPPFILES)) 31 | 32 | all: $(NAME) 33 | 34 | ifneq ($(OS),Windows_NT) 35 | ifeq ($(uname -s),Darwin) 36 | 37 | install: $(NAME) 38 | ./mac-bundle.sh 39 | cp -r $(APPNAME).app /Applications/ 40 | 41 | uninstall: 42 | rm -rf /Applications/$(APPNAME).app 43 | 44 | else 45 | 46 | flatpak: 47 | flatpak-builder --repo=repo --force-clean build-flatpak $(PKGNAME).yml 48 | flatpak build-bundle repo $(NAME).flatpak $(PKGNAME) 49 | 50 | flatpak-clean: 51 | rm -rf .flatpak-builder 52 | rm -rf build-flatpak 53 | rm -rf repo 54 | rm -f $(NAME).flatpak 55 | 56 | install: $(NAME) 57 | install -Dm755 $(NAME) "$(DESTDIR)/bin/$(NAME)" 58 | install -Dm644 $(PKGNAME).desktop "$(DESTDIR)/share/applications/$(PKGNAME).desktop" 59 | 60 | uninstall: 61 | rm -f "$(DESTDIR)/bin/$(NAME)" 62 | rm -f "$(DESTDIR)/share/applications/$(PKGNAME).desktop" 63 | 64 | endif 65 | endif 66 | 67 | $(NAME): $(OFILES) 68 | g++ -o $@ $(ARGS) $^ $(LIBS) 69 | 70 | $(BUILD)/%.o: %.cpp $(HFILES) $(BUILD) 71 | g++ -c -o $@ $(ARGS) $(INCS) $< 72 | 73 | $(BUILD): 74 | for dir in $(SRCS); do mkdir -p $(BUILD)/$$dir; done 75 | 76 | switch: 77 | $(MAKE) -f Makefile.switch 78 | 79 | clean: 80 | if [ -d "build-switch" ]; then $(MAKE) -f Makefile.switch clean; fi 81 | rm -rf $(BUILD) 82 | rm -f $(NAME) 83 | -------------------------------------------------------------------------------- /src/desktop/ry_frame.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef RY_FRAME_H 21 | #define RY_FRAME_H 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #define MIN_SIZE wxSize(480, 360) 28 | 29 | class ryCanvas; 30 | 31 | class ryFrame: public wxFrame 32 | { 33 | public: 34 | ryFrame(std::string path); 35 | 36 | void Refresh(); 37 | bool isPaused() { return paused; } 38 | 39 | void pressKey(int key); 40 | void releaseKey(int key); 41 | 42 | private: 43 | ryCanvas *canvas; 44 | wxMenu *fileMenu; 45 | wxMenu *systemMenu; 46 | wxJoystick *joystick; 47 | wxTimer *timer; 48 | 49 | std::string lastPath; 50 | bool paused = false; 51 | std::vector axisBases; 52 | bool stickPressed[5] = {}; 53 | 54 | void bootRom(std::string path); 55 | void updateMenu(); 56 | void updateKeyStick(); 57 | 58 | void loadRom(wxCommandEvent &event); 59 | void changeSave(wxCommandEvent &event); 60 | void quit(wxCommandEvent &event); 61 | void pause(wxCommandEvent &event); 62 | void restart(wxCommandEvent &event); 63 | void stop(wxCommandEvent &event); 64 | void inputSettings(wxCommandEvent &event); 65 | void toggleFpsLimit(wxCommandEvent &event); 66 | void toggleExpanPak(wxCommandEvent &event); 67 | void toggleThreadRdp(wxCommandEvent &event); 68 | void toggleTexFilter(wxCommandEvent &event); 69 | void updateJoystick(wxTimerEvent &event); 70 | void dropFiles(wxDropFilesEvent &event); 71 | void close(wxCloseEvent &event); 72 | 73 | wxDECLARE_EVENT_TABLE(); 74 | }; 75 | 76 | #endif // RY_FRAME_H 77 | -------------------------------------------------------------------------------- /src/desktop/input_dialog.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef INPUT_DIALOG_H 21 | #define INPUT_DIALOG_H 22 | 23 | #include "ry_app.h" 24 | 25 | class InputDialog: public wxDialog 26 | { 27 | public: 28 | InputDialog(wxJoystick *joystick); 29 | ~InputDialog(); 30 | 31 | private: 32 | wxJoystick *joystick; 33 | wxTimer *timer; 34 | wxButton *keys[MAX_KEYS]; 35 | 36 | int keyBinds[MAX_KEYS]; 37 | std::vector axisBases; 38 | wxButton *current = nullptr; 39 | int keyIndex = 0; 40 | 41 | std::string keyToString(int key); 42 | void resetLabels(); 43 | 44 | void remapA(wxCommandEvent &event); 45 | void remapB(wxCommandEvent &event); 46 | void remapZ(wxCommandEvent &event); 47 | void remapStart(wxCommandEvent &event); 48 | void remapDUp(wxCommandEvent &event); 49 | void remapDDown(wxCommandEvent &event); 50 | void remapDLeft(wxCommandEvent &event); 51 | void remapDRight(wxCommandEvent &event); 52 | void remapL(wxCommandEvent &event); 53 | void remapR(wxCommandEvent &event); 54 | void remapCUp(wxCommandEvent &event); 55 | void remapCDown(wxCommandEvent &event); 56 | void remapCLeft(wxCommandEvent &event); 57 | void remapCRight(wxCommandEvent &event); 58 | void remapSUp(wxCommandEvent &event); 59 | void remapSDown(wxCommandEvent &event); 60 | void remapSLeft(wxCommandEvent &event); 61 | void remapSRight(wxCommandEvent &event); 62 | void remapSMod(wxCommandEvent &event); 63 | void remapFullScreen(wxCommandEvent &event); 64 | 65 | void clearMap(wxCommandEvent &event); 66 | void updateJoystick(wxTimerEvent &event); 67 | void confirm(wxCommandEvent &event); 68 | void pressKey(wxKeyEvent &event); 69 | 70 | wxDECLARE_EVENT_TABLE(); 71 | }; 72 | 73 | #endif // INPUT_DIALOG_H 74 | -------------------------------------------------------------------------------- /src/mi.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "mi.h" 21 | #include "cpu_cp0.h" 22 | #include "log.h" 23 | 24 | namespace MI 25 | { 26 | uint32_t interrupt; 27 | uint32_t mask; 28 | } 29 | 30 | void MI::reset() 31 | { 32 | // Reset the MI to its initial state 33 | interrupt = 0; 34 | mask = 0; 35 | } 36 | 37 | uint32_t MI::read(uint32_t address) 38 | { 39 | // Read from an I/O register if one exists at the given address 40 | switch (address) 41 | { 42 | case 0x4300008: // MI_INTERRUPT 43 | // Get the interrupt flags 44 | return interrupt; 45 | 46 | case 0x430000C: // MI_MASK 47 | // Get the interrupt mask 48 | return mask; 49 | 50 | default: 51 | LOG_WARN("Unknown MI register read: 0x%X\n", address); 52 | return 0; 53 | } 54 | } 55 | 56 | void MI::write(uint32_t address, uint32_t value) 57 | { 58 | // Write to an I/O register if one exists at the given address 59 | switch (address) 60 | { 61 | case 0x4300000: // MI_MODE 62 | // Acknowledge a DP interrupt when bit 11 is set 63 | if (value & 0x800) 64 | clearInterrupt(5); 65 | 66 | // Keep track of unimplemented bits that should do something 67 | if (uint32_t bits = (value & 0x37FF)) 68 | LOG_WARN("Unimplemented MI mode bits set: 0x%X\n", bits); 69 | return; 70 | 71 | case 0x430000C: // MI_MASK 72 | // For each set bit, set or clear a mask bit appropriately 73 | for (int i = 0; i < 12; i += 2) 74 | { 75 | if (value & (1 << i)) 76 | mask &= ~(1 << (i / 2)); 77 | else if (value & (1 << (i + 1))) 78 | mask |= (1 << (i / 2)); 79 | } 80 | 81 | CPU_CP0::checkInterrupts(); 82 | return; 83 | 84 | default: 85 | LOG_WARN("Unknown MI register write: 0x%X\n", address); 86 | return; 87 | } 88 | } 89 | 90 | void MI::setInterrupt(int bit) 91 | { 92 | // Request an interrupt by setting its bit 93 | interrupt |= (1 << bit); 94 | CPU_CP0::checkInterrupts(); 95 | } 96 | 97 | void MI::clearInterrupt(int bit) 98 | { 99 | // Acknowledge an interrupt by clearing its bit 100 | interrupt &= ~(1 << bit); 101 | CPU_CP0::checkInterrupts(); 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rokuyon 2 | An experimental N64 emulator 3 | 4 | ### Overview 5 | My main goal with rokuyon is to learn about the N64's hardware so I can write homebrew like 6 | [sodium64](https://github.com/Hydr8gon/sodium64). If it ends up being more than that, I wouldn't mind making a modern, 7 | accurate N64 emulator with built-in software/hardware rendering and no messy plugins. 8 | 9 | ### Downloads 10 | rokuyon is available for Windows, macOS, Linux, and Switch. The latest builds are automatically provided via GitHub 11 | Actions, and can be downloaded from the [releases page](https://github.com/Hydr8gon/rokuyon/releases). 12 | 13 | ### Usage 14 | No setup is required to run things in rokuyon, but right now it only supports NTSC ROMs in big-endian format. Save types 15 | are not automatically detected, and must be manually selected in the file menu to work. Performance will be bad without 16 | a powerful CPU, and you'll probably encounter plenty of emulation issues. At this stage, rokuyon should be considered a 17 | curiosity and not a dedicated emulator for playing games. 18 | 19 | ### Contributing 20 | This is a personal project, and I've decided to not review or accept pull requests for it. If you want to help, you can 21 | test things and report issues or provide feedback. If you can afford it, you can also donate to motivate me and allow me 22 | to spend more time on things like this. Nothing is mandatory, and I appreciate any interest in my projects, even if 23 | you're just a user! 24 | 25 | ### Building 26 | **Windows:** Install [MSYS2](https://www.msys2.org) and run the command 27 | `pacman -Syu mingw-w64-x86_64-{gcc,pkg-config,wxWidgets,portaudio,jbigkit} make` to get dependencies. Navigate to the 28 | project root directory and run `make -j$(nproc)` to start building. 29 | 30 | **macOS/Linux:** On the target system, install [wxWidgets](https://www.wxwidgets.org) and 31 | [PortAudio](https://www.portaudio.com). This can be done with the [Homebrew](https://brew.sh) package manager on macOS, 32 | or a built-in package manager on Linux. Run `make -j$(nproc)` in the project root directory to start building. 33 | 34 | **Switch:** Install [devkitPro](https://devkitpro.org/wiki/Getting_Started) and its `switch-dev` package. Run 35 | `make switch -j$(nproc)` in the project root directory to start building. 36 | 37 | ### Hardware References 38 | * [N64brew Wiki](https://n64brew.dev/wiki/Main_Page) - Extensive documentation of both hardware and software 39 | * [RSP Vector Instructions](https://emudev.org/2020/03/28/RSP.html) - Detailed information on how vector opcodes work 40 | * [RCP Documentation](https://dragonminded.com/n64dev/Reality%20Coprocessor.pdf) - Nice reference for a subset of RDP 41 | functionality 42 | * [RDP Triangle Command Guide](https://docs.google.com/document/d/17ddEo61V0suXbSkKP5mY97QxgUnB-QfAjuBIsPiLWko) - Covers 43 | everything related to RDP triangles 44 | * [n64-systemtest](https://github.com/lemmy-64/n64-systemtest) - Comprehensive tests that target all parts of the system 45 | * [n64dev](https://github.com/mikeryan/n64dev) - A collection of useful documents and source code 46 | 47 | ### Other Links 48 | * [Hydra's Lair](https://hydr8gon.github.io) - Blog where I may or may not write about things 49 | * [Discord Server](https://discord.gg/JbNz7y4) - A place to chat about my projects and stuff 50 | -------------------------------------------------------------------------------- /.github/workflows/autobuild.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Builds 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Install Flatpak and SDK 14 | run: | 15 | sudo apt update 16 | sudo apt install flatpak flatpak-builder -y 17 | sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 18 | sudo flatpak install flathub org.freedesktop.Platform//21.08 org.freedesktop.Sdk//21.08 -y 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Compile 22 | run: | 23 | git config --global protocol.file.allow always 24 | make flatpak -j$(nproc) 25 | - name: Upload 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: rokuyon-linux 29 | path: rokuyon.flatpak 30 | 31 | build-mac: 32 | runs-on: macos-latest 33 | 34 | steps: 35 | - name: Install wxWidgets and PortAudio 36 | run: brew install wxmac portaudio 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | - name: Compile 40 | run: | 41 | make -j$(sysctl -n hw.logicalcpu) 42 | ./mac-bundle.sh --dmg 43 | - name: Upload 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: rokuyon-mac 47 | path: rokuyon.dmg 48 | 49 | build-windows: 50 | runs-on: windows-latest 51 | 52 | steps: 53 | - name: Checkout 54 | uses: actions/checkout@v4 55 | - name: Install MSYS2 56 | uses: msys2/setup-msys2@v2 57 | with: 58 | msystem: MINGW64 59 | update: true 60 | - name: Install build tools, wxWidgets, and PortAudio 61 | run: pacman -S mingw-w64-x86_64-{gcc,pkg-config,wxWidgets,portaudio,jbigkit} make --noconfirm 62 | shell: msys2 {0} 63 | - name: Compile 64 | run: | 65 | make -j$(nproc) 66 | strip rokuyon.exe 67 | shell: msys2 {0} 68 | working-directory: ${{ github.workspace }} 69 | - name: Upload 70 | uses: actions/upload-artifact@v4 71 | with: 72 | name: rokuyon-windows 73 | path: rokuyon.exe 74 | 75 | build-switch: 76 | runs-on: ubuntu-latest 77 | container: devkitpro/devkita64:latest 78 | 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v4 82 | - name: Compile 83 | run: make switch -j$(nproc) 84 | - name: Upload 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: rokuyon-switch 88 | path: rokuyon.nro 89 | 90 | update-release: 91 | runs-on: ubuntu-latest 92 | needs: [build-linux, build-mac, build-windows, build-switch] 93 | 94 | steps: 95 | - name: Delete old release 96 | uses: dev-drprasad/delete-tag-and-release@v0.2.1 97 | with: 98 | delete_release: true 99 | tag_name: release 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | - name: Get artifacts 103 | uses: actions/download-artifact@v4 104 | - name: Package artifacts 105 | run: for i in ./*; do zip -r -j ${i}.zip $i; done 106 | - name: Create new release 107 | uses: ncipollo/release-action@v1 108 | with: 109 | name: Rolling Release 110 | body: These are automatically updated builds of the latest commit. 111 | artifacts: "*.zip" 112 | tag: release 113 | token: ${{ secrets.GITHUB_TOKEN }} 114 | -------------------------------------------------------------------------------- /src/settings.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | 22 | #include "settings.h" 23 | 24 | struct Setting 25 | { 26 | Setting(std::string name, void *value, bool isString): 27 | name(name), value(value), isString(isString) {} 28 | 29 | std::string name; 30 | void *value; 31 | bool isString; 32 | }; 33 | 34 | namespace Settings 35 | { 36 | std::string filename; 37 | int fpsLimiter = 1; 38 | int expansionPak = 1; 39 | int threadedRdp = 0; 40 | int texFilter = 1; 41 | 42 | std::vector settings = 43 | { 44 | Setting("fpsLimiter", &fpsLimiter, false), 45 | Setting("expansionPak", &expansionPak, false), 46 | Setting("threadedRdp", &threadedRdp, false), 47 | Setting("texFilter", &texFilter, false) 48 | }; 49 | } 50 | 51 | void Settings::add(std::string name, void *value, bool isString) 52 | { 53 | // Add an additional platform setting to be loaded from the settings file 54 | settings.push_back(Setting(name, value, isString)); 55 | } 56 | 57 | bool Settings::load(std::string filename) 58 | { 59 | // Attempt to open the settings file; otherwise default values will be used 60 | Settings::filename = filename; 61 | FILE *file = fopen(filename.c_str(), "r"); 62 | if (!file) return false; 63 | 64 | char data[1024]; 65 | 66 | // Read each line in the settings file and load values from them 67 | while (fgets(data, 1024, file) != nullptr) 68 | { 69 | std::string line = data; 70 | int split = line.find("="); 71 | std::string name = line.substr(0, split); 72 | 73 | for (size_t i = 0; i < settings.size(); i++) 74 | { 75 | if (name == settings[i].name) 76 | { 77 | std::string value = line.substr(split + 1, line.size() - split - 2); 78 | if (settings[i].isString) 79 | *(std::string*)settings[i].value = value; 80 | else if (value[0] >= '0' && value[0] <= '9') 81 | *(int*)settings[i].value = stoi(value); 82 | break; 83 | } 84 | } 85 | } 86 | 87 | fclose(file); 88 | return true; 89 | } 90 | 91 | bool Settings::save() 92 | { 93 | // Attempt to open the settings file 94 | FILE *file = fopen(filename.c_str(), "w"); 95 | if (!file) return false; 96 | 97 | // Write each value to a line in the settings file 98 | for (size_t i = 0; i < settings.size(); i++) 99 | { 100 | std::string value = settings[i].isString ? 101 | *(std::string*)settings[i].value : std::to_string(*(int*)settings[i].value); 102 | fprintf(file, "%s=%s\n", settings[i].name.c_str(), value.c_str()); 103 | } 104 | 105 | fclose(file); 106 | return true; 107 | } 108 | -------------------------------------------------------------------------------- /src/switch/switch_ui.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #ifndef SWITCH_UI_H 21 | #define SWITCH_UI_H 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | struct ListItem 28 | { 29 | ListItem(std::string name, std::string setting = "", uint32_t *icon = nullptr, int iconSize = 0): 30 | name(name), setting(setting), icon(icon), iconSize(iconSize) {} 31 | 32 | std::string name; 33 | std::string setting; 34 | uint32_t *icon; 35 | int iconSize; 36 | 37 | bool operator < (const ListItem &item) { return (name < item.name); } 38 | }; 39 | 40 | struct Selection 41 | { 42 | Selection(uint32_t pressed, size_t index): pressed(pressed), index(index) {} 43 | 44 | uint32_t pressed; 45 | size_t index; 46 | }; 47 | 48 | struct Color 49 | { 50 | Color(uint8_t r, uint8_t g, uint8_t b): r(r), g(g), b(b) {} 51 | Color(): r(0), g(0), b(0) {} 52 | 53 | uint8_t r, g, b; 54 | }; 55 | 56 | class SwitchUI 57 | { 58 | public: 59 | static void initialize(); 60 | static void deinitialize(); 61 | 62 | static uint32_t *bmpToTexture(std::string filename); 63 | 64 | static void drawImage(uint32_t *image, int width, int height, int x, int y, int scaleWidth, int scaleHeight, bool filter = true, int rotation = 0); 65 | static void drawString(std::string string, int x, int y, int size, Color color, bool alignRight = false); 66 | static void drawRectangle(int x, int y, int width, int height, Color color); 67 | static void clear(Color color); 68 | static void update(); 69 | 70 | static Selection menu(std::string title, std::vector *items, size_t index = 0, 71 | std::string actionX = "", std::string actionPlus = ""); 72 | static bool message(std::string title, std::vector text, bool cancel = false); 73 | 74 | static bool isDarkTheme() { return darkTheme; } 75 | static PadState *getPad() { return &pad; } 76 | 77 | private: 78 | SwitchUI() {} // Private to prevent instantiation 79 | 80 | static bool shouldExit; 81 | 82 | static EGLDisplay display; 83 | static EGLContext context; 84 | static EGLSurface surface; 85 | 86 | static GLuint program; 87 | static GLuint vbo; 88 | static GLuint textures[3]; 89 | 90 | static const char *vertexShader; 91 | static const char *fragmentShader; 92 | 93 | static const uint32_t *font; 94 | static const uint32_t empty; 95 | 96 | static const int charWidths[]; 97 | 98 | static bool darkTheme; 99 | static Color palette[6]; 100 | 101 | static PadState pad; 102 | static bool touchMode; 103 | 104 | static int stringWidth(std::string string); 105 | }; 106 | 107 | #endif // SWITCH_UI_H 108 | -------------------------------------------------------------------------------- /src/si.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "si.h" 21 | #include "log.h" 22 | #include "memory.h" 23 | #include "mi.h" 24 | #include "pif.h" 25 | 26 | namespace SI 27 | { 28 | uint32_t dramAddr; 29 | 30 | void performReadDma(uint32_t address); 31 | void performWriteDma(uint32_t address); 32 | } 33 | 34 | void SI::reset() 35 | { 36 | // Reset the SI to its initial state 37 | dramAddr = 0; 38 | } 39 | 40 | uint32_t SI::read(uint32_t address) 41 | { 42 | // Read from an I/O register if one exists at the given address 43 | switch (address) 44 | { 45 | default: 46 | LOG_WARN("Unknown SI register read: 0x%X\n", address); 47 | return 0; 48 | } 49 | } 50 | 51 | void SI::write(uint32_t address, uint32_t value) 52 | { 53 | // Write to an I/O register if one exists at the given address 54 | switch (address) 55 | { 56 | case 0x4800000: // SI_DRAM_ADDR 57 | // Set the RDRAM DMA address 58 | dramAddr = value & 0xFFFFFF; 59 | return; 60 | 61 | case 0x4800004: // SI_PIF_AD_RD64B 62 | // Start a DMA transfer from PIF to RDRAM 63 | performReadDma(value & 0x7FC); 64 | return; 65 | 66 | case 0x4800010: // SI_PIF_AD_WR64B 67 | // Start a DMA transfer from RDRAM to PIF 68 | performWriteDma(value & 0x7FC); 69 | return; 70 | 71 | case 0x4800018: // SI_STATUS 72 | // Acknowledge an SI interrupt 73 | MI::clearInterrupt(1); 74 | return; 75 | 76 | default: 77 | LOG_WARN("Unknown SI register write: 0x%X\n", address); 78 | return; 79 | } 80 | } 81 | 82 | void SI::performReadDma(uint32_t address) 83 | { 84 | LOG_INFO("SI DMA from PIF 0x%X to RDRAM 0x%X with size 0x40\n", address, dramAddr); 85 | 86 | // Re-trigger the last PIF command on DMA reads 87 | // TODO: properly look into how PIF command triggers work 88 | PIF::runCommand(); 89 | 90 | // Copy 64 bytes from PIF RAM to RDRAM 91 | for (uint32_t i = 0; i < 0x40; i++) 92 | { 93 | uint8_t value = Memory::read(0x9FC00000 + address + i); 94 | Memory::write(0x80000000 + dramAddr + i, value); 95 | } 96 | 97 | // Request an SI interrupt when the DMA finishes 98 | // TODO: make DMAs not instant 99 | MI::setInterrupt(1); 100 | } 101 | 102 | void SI::performWriteDma(uint32_t address) 103 | { 104 | LOG_INFO("SI DMA from RDRAM 0x%X to PIF 0x%X with size 0x40\n", dramAddr, address); 105 | 106 | // Copy 64 bytes from RDRAM to PIF RAM 107 | for (uint32_t i = 0; i < 0x40; i++) 108 | { 109 | uint8_t value = Memory::read(0x80000000 + dramAddr + i); 110 | Memory::write(0x9FC00000 + address + i, value); 111 | } 112 | 113 | // Request an SI interrupt when the DMA finishes 114 | // TODO: make DMAs not instant 115 | MI::setInterrupt(1); 116 | } 117 | -------------------------------------------------------------------------------- /src/pi.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | 22 | #include "pi.h" 23 | #include "log.h" 24 | #include "memory.h" 25 | #include "mi.h" 26 | 27 | namespace PI 28 | { 29 | uint32_t dramAddr; 30 | uint32_t cartAddr; 31 | 32 | void performReadDma(uint32_t length); 33 | void performWriteDma(uint32_t length); 34 | } 35 | 36 | void PI::reset() 37 | { 38 | // Reset the PI to its initial state 39 | dramAddr = 0; 40 | cartAddr = 0; 41 | } 42 | 43 | uint32_t PI::read(uint32_t address) 44 | { 45 | // Read from an I/O register if one exists at the given address 46 | switch (address) 47 | { 48 | default: 49 | LOG_WARN("Unknown PI register read: 0x%X\n", address); 50 | return 0; 51 | } 52 | } 53 | 54 | void PI::write(uint32_t address, uint32_t value) 55 | { 56 | // Write to an I/O register if one exists at the given address 57 | switch (address) 58 | { 59 | case 0x4600000: // PI_DRAM_ADDR 60 | // Set the RDRAM DMA address 61 | dramAddr = value & 0xFFFFFF; 62 | return; 63 | 64 | case 0x4600004: // PI_CART_ADDR 65 | // Set the cart DMA address 66 | cartAddr = value; 67 | return; 68 | 69 | case 0x4600008: // PI_RD_LEN 70 | // Start a DMA transfer from RDRAM to PI 71 | performWriteDma((value & 0xFFFFFF) + 1); 72 | return; 73 | 74 | case 0x460000C: // PI_WR_LEN 75 | // Start a DMA transfer from PI to RDRAM 76 | performReadDma((value & 0xFFFFFF) + 1); 77 | return; 78 | 79 | case 0x4600010: // PI_STATUS 80 | // Acknowledge a PI interrupt when bit 1 is set 81 | // TODO: handle bit 0 82 | if (value & 0x2) 83 | MI::clearInterrupt(4); 84 | return; 85 | 86 | default: 87 | LOG_WARN("Unknown PI register write: 0x%X\n", address); 88 | return; 89 | } 90 | } 91 | 92 | void PI::performReadDma(uint32_t size) 93 | { 94 | LOG_INFO("PI DMA from cart 0x%X to RDRAM 0x%X with size 0x%X\n", cartAddr, dramAddr, size); 95 | 96 | // Copy data from the PI bus to memory 97 | // TODO: check bounds 98 | for (uint32_t i = 0; i < size; i++) 99 | { 100 | uint8_t value = Memory::read(0x80000000 + cartAddr + i); 101 | Memory::write(0x80000000 + dramAddr + i, value); 102 | } 103 | 104 | // Request a PI interrupt when the DMA finishes 105 | // TODO: make DMAs not instant 106 | MI::setInterrupt(4); 107 | } 108 | 109 | 110 | void PI::performWriteDma(uint32_t size) 111 | { 112 | LOG_INFO("PI DMA from RDRAM 0x%X to cart 0x%X with size 0x%X\n", dramAddr, cartAddr, size); 113 | 114 | // Copy data from memory to the PI bus 115 | // TODO: check bounds 116 | for (uint32_t i = 0; i < size; i++) 117 | { 118 | uint8_t value = Memory::read(0x80000000 + dramAddr + i); 119 | Memory::write(0x80000000 + cartAddr + i, value); 120 | } 121 | 122 | // Request a PI interrupt when the DMA finishes 123 | // TODO: make DMAs not instant 124 | MI::setInterrupt(4); 125 | } 126 | -------------------------------------------------------------------------------- /src/desktop/ry_app.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | 23 | #include "ry_app.h" 24 | #include "../ai.h" 25 | #include "../core.h" 26 | #include "../settings.h" 27 | 28 | enum AppEvent 29 | { 30 | UPDATE = 1 31 | }; 32 | 33 | wxBEGIN_EVENT_TABLE(ryApp, wxApp) 34 | EVT_TIMER(UPDATE, ryApp::update) 35 | wxEND_EVENT_TABLE() 36 | 37 | int ryApp::keyBinds[] = 38 | { 39 | 'L', 'K', 'J', 'G', // A, B, Z, Start 40 | WXK_UP, WXK_DOWN, WXK_LEFT, WXK_RIGHT, // D-pad 41 | 'Q', 'P', // L, R 42 | '8', 'I', 'U', 'O', // C-buttons 43 | 'W', 'S', 'A', 'D', WXK_SHIFT, // Joystick 44 | WXK_ESCAPE // Full screen 45 | }; 46 | 47 | bool ryApp::OnInit() 48 | { 49 | // Define the input binding setting names 50 | static const char *names[MAX_KEYS] = 51 | { 52 | "keyA", "keyB", "keyZ", "keyStart", 53 | "keyDUp", "keyDDown", "keyDLeft", "keyDRight", 54 | "keyL", "keyR", 55 | "keyCUp", "keyCDown", "keyCLeft", "keyCRight", 56 | "keySUp", "keySDown", "keySLeft", "keySRight", 57 | "keySMod", "keyFullScreen" 58 | }; 59 | 60 | // Register the input binding settings 61 | for (int i = 0; i < MAX_KEYS; i++) 62 | Settings::add(names[i], &keyBinds[i], false); 63 | 64 | // Try to load settings from the current directory first 65 | if (!Settings::load()) 66 | { 67 | // Get the system-specific application settings directory 68 | std::string settingsDir; 69 | wxStandardPaths &paths = wxStandardPaths::Get(); 70 | #if defined(WINDOWS) || defined(MACOS) || !wxCHECK_VERSION(3, 1, 0) 71 | settingsDir = paths.GetUserDataDir().mb_str(wxConvUTF8); 72 | #else 73 | paths.SetFileLayout(wxStandardPaths::FileLayout_XDG); 74 | settingsDir = paths.GetUserConfigDir().mb_str(wxConvUTF8); 75 | settingsDir += "/rokuyon"; 76 | #endif 77 | 78 | // Try to load settings from the system directory, creating it if it doesn't exist 79 | if (!Settings::load(settingsDir + "/rokuyon.ini")) 80 | { 81 | wxFileName dir = wxFileName::DirName(settingsDir); 82 | if (!dir.DirExists()) dir.Mkdir(); 83 | Settings::save(); 84 | } 85 | } 86 | 87 | // Create the app's frame, passing along a filename from the command line 88 | SetAppName("rokuyon"); 89 | frame = new ryFrame((argc > 1) ? argv[1].ToStdString() : ""); 90 | 91 | // Set up the update timer 92 | timer = new wxTimer(this, UPDATE); 93 | timer->Start(6); 94 | 95 | // Set up the audio stream 96 | Pa_Initialize(); 97 | Pa_OpenDefaultStream(&stream, 0, 2, paInt16, 48000, 1024, audioCallback, nullptr); 98 | Pa_StartStream(stream); 99 | 100 | return true; 101 | } 102 | 103 | int ryApp::OnExit() 104 | { 105 | // Stop some things before exiting 106 | Pa_StopStream(stream); 107 | timer->Stop(); 108 | return wxApp::OnExit(); 109 | } 110 | 111 | void ryApp::update(wxTimerEvent &event) 112 | { 113 | // Continuously refresh the frame 114 | frame->Refresh(); 115 | } 116 | 117 | int ryApp::audioCallback(const void *in, void *out, unsigned long count, 118 | const PaStreamCallbackTimeInfo *info, PaStreamCallbackFlags flags, void *data) 119 | { 120 | // Get samples from the audio interface 121 | AI::fillBuffer((uint32_t*)out); 122 | return paContinue; 123 | } 124 | -------------------------------------------------------------------------------- /src/desktop/save_dialog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "save_dialog.h" 21 | #include "../core.h" 22 | 23 | enum SaveEvent 24 | { 25 | SELECT_0 = 1, 26 | SELECT_1, 27 | SELECT_2, 28 | SELECT_3, 29 | SELECT_4 30 | }; 31 | 32 | wxBEGIN_EVENT_TABLE(SaveDialog, wxDialog) 33 | EVT_RADIOBUTTON(SELECT_0, SaveDialog::select0) 34 | EVT_RADIOBUTTON(SELECT_1, SaveDialog::select1) 35 | EVT_RADIOBUTTON(SELECT_2, SaveDialog::select2) 36 | EVT_RADIOBUTTON(SELECT_3, SaveDialog::select3) 37 | EVT_RADIOBUTTON(SELECT_4, SaveDialog::select4) 38 | EVT_BUTTON(wxID_OK, SaveDialog::confirm) 39 | wxEND_EVENT_TABLE() 40 | 41 | uint32_t SaveDialog::selectToSize(uint32_t select) 42 | { 43 | // Convert a save selection to a size 44 | switch (select) 45 | { 46 | case 1: return 0x00200; // EEPROM 0.5KB 47 | case 2: return 0x00800; // EEPROM 8KB 48 | case 3: return 0x08000; // SRAM 32KB 49 | case 4: return 0x20000; // FLASH 128KB 50 | default: return 0x00000; // None 51 | } 52 | } 53 | 54 | uint32_t SaveDialog::sizeToSelect(uint32_t size) 55 | { 56 | // Convert a save size to a selection 57 | switch (size) 58 | { 59 | case 0x00200: return 1; // EEPROM 0.5KB 60 | case 0x00800: return 2; // EEPROM 8KB 61 | case 0x08000: return 3; // SRAM 32KB 62 | case 0x20000: return 4; // FLASH 128KB 63 | default: return 0; // None 64 | } 65 | } 66 | 67 | SaveDialog::SaveDialog(std::string &lastPath): lastPath(lastPath), 68 | wxDialog(nullptr, wxID_ANY, "Change Save Type") 69 | { 70 | // Get the height of a button in pixels as a reference scale for the rest of the UI 71 | wxButton *dummy = new wxButton(this, wxID_ANY, ""); 72 | size_t scale = dummy->GetSize().y; 73 | delete dummy; 74 | 75 | // Create left and right columns for the radio buttons 76 | wxBoxSizer *leftRadio = new wxBoxSizer(wxVERTICAL); 77 | wxBoxSizer *rightRadio = new wxBoxSizer(wxVERTICAL); 78 | wxRadioButton *buttons[5]; 79 | 80 | // Set up radio buttons for the save types 81 | leftRadio->Add(buttons[0] = new wxRadioButton(this, SELECT_0, "None"), 1); 82 | leftRadio->Add(buttons[1] = new wxRadioButton(this, SELECT_1, "EEPROM 0.5KB"), 1); 83 | leftRadio->Add(buttons[2] = new wxRadioButton(this, SELECT_2, "EEPROM 2KB"), 1); 84 | rightRadio->Add(buttons[3] = new wxRadioButton(this, SELECT_3, "SRAM 32KB"), 1); 85 | rightRadio->Add(buttons[4] = new wxRadioButton(this, SELECT_4, "FLASH 128KB"), 1); 86 | rightRadio->Add(new wxStaticText(this, wxID_ANY, ""), 1); 87 | 88 | // Select the current save type by default 89 | selection = sizeToSelect(Core::saveSize); 90 | buttons[selection]->SetValue(true); 91 | 92 | // Combine all of the radio buttons 93 | wxBoxSizer *radioSizer = new wxBoxSizer(wxHORIZONTAL); 94 | radioSizer->Add(leftRadio, 1, wxEXPAND | wxRIGHT, scale / 8); 95 | radioSizer->Add(rightRadio, 1, wxEXPAND | wxLEFT, scale / 8); 96 | 97 | // Set up the cancel and confirm buttons 98 | wxBoxSizer *buttonSizer = new wxBoxSizer(wxHORIZONTAL); 99 | buttonSizer->Add(new wxStaticText(this, wxID_ANY, ""), 1); 100 | buttonSizer->Add(new wxButton(this, wxID_CANCEL, "Cancel"), 0, wxRIGHT, scale / 16); 101 | buttonSizer->Add(new wxButton(this, wxID_OK, "Confirm"), 0, wxLEFT, scale / 16); 102 | 103 | // Combine all of the contents 104 | wxBoxSizer *contents = new wxBoxSizer(wxVERTICAL); 105 | contents->Add(radioSizer, 1, wxEXPAND | wxALL, scale / 8); 106 | contents->Add(buttonSizer, 0, wxEXPAND | wxALL, scale / 8); 107 | 108 | // Add a final border around everything 109 | wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); 110 | sizer->Add(contents, 1, wxEXPAND | wxALL, scale / 8); 111 | SetSizer(sizer); 112 | 113 | // Size the window to fit the contents and prevent resizing 114 | sizer->Fit(this); 115 | SetMinSize(GetSize()); 116 | SetMaxSize(GetSize()); 117 | } 118 | 119 | void SaveDialog::select0(wxCommandEvent &event) 120 | { 121 | // Select save type 0 122 | selection = 0; 123 | } 124 | 125 | void SaveDialog::select1(wxCommandEvent &event) 126 | { 127 | // Select save type 1 128 | selection = 1; 129 | } 130 | 131 | void SaveDialog::select2(wxCommandEvent &event) 132 | { 133 | // Select save type 2 134 | selection = 2; 135 | } 136 | 137 | void SaveDialog::select3(wxCommandEvent &event) 138 | { 139 | // Select save type 3 140 | selection = 3; 141 | } 142 | 143 | void SaveDialog::select4(wxCommandEvent &event) 144 | { 145 | // Select save type 4 146 | selection = 4; 147 | } 148 | 149 | void SaveDialog::confirm(wxCommandEvent &event) 150 | { 151 | // Ask for confirmation before doing anything because accidents could be bad! 152 | wxMessageDialog dialog(this, "Are you sure? This may result in data loss!", 153 | "Changing Save Type", wxYES_NO | wxICON_NONE); 154 | 155 | // On confirmation, change the save type and restart the emulator 156 | if (dialog.ShowModal() == wxID_YES) 157 | { 158 | Core::stop(); 159 | Core::resizeSave(selectToSize(selection)); 160 | Core::bootRom(lastPath); 161 | event.Skip(true); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/desktop/ry_canvas.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "ry_canvas.h" 21 | #include "ry_app.h" 22 | #include "../core.h" 23 | #include "../vi.h" 24 | 25 | #ifdef _WIN32 26 | #include 27 | #include 28 | #endif 29 | 30 | wxBEGIN_EVENT_TABLE(ryCanvas, wxGLCanvas) 31 | EVT_PAINT(ryCanvas::draw) 32 | EVT_SIZE(ryCanvas::resize) 33 | EVT_KEY_DOWN(ryCanvas::pressKey) 34 | EVT_KEY_UP(ryCanvas::releaseKey) 35 | wxEND_EVENT_TABLE() 36 | 37 | ryCanvas::ryCanvas(ryFrame *frame): wxGLCanvas(frame, wxID_ANY, nullptr), frame(frame) 38 | { 39 | // Prepare the OpenGL context 40 | context = new wxGLContext(this); 41 | 42 | // Set focus so that key presses will be registered 43 | SetFocus(); 44 | } 45 | 46 | void ryCanvas::finish() 47 | { 48 | // Tell the canvas to stop rendering 49 | finished = true; 50 | } 51 | 52 | void ryCanvas::draw(wxPaintEvent &event) 53 | { 54 | // Stop rendering so the program can close 55 | if (finished) 56 | return; 57 | 58 | SetCurrent(*context); 59 | static bool setup = false; 60 | 61 | if (!setup) 62 | { 63 | // Prepare a texture for the framebuffer 64 | GLuint texture; 65 | glEnable(GL_TEXTURE_2D); 66 | glGenTextures(1, &texture); 67 | glBindTexture(GL_TEXTURE_2D, texture); 68 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 69 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 70 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 71 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 72 | 73 | // Finish initial setup 74 | frame->SendSizeEvent(); 75 | setup = true; 76 | } 77 | 78 | // Clear the screen 79 | glClearColor(0.0f, 0.0f, 0.0f, 1.0f); 80 | glClear(GL_COLOR_BUFFER_BIT); 81 | 82 | if (Core::running || frame->isPaused()) 83 | { 84 | // At the swap interval, get the framebuffer as a texture 85 | if (++frameCount >= swapInterval) 86 | { 87 | if (_Framebuffer *fb = VI::getFramebuffer()) 88 | { 89 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fb->width, 90 | fb->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, fb->data); 91 | frameCount = 0; 92 | delete fb; 93 | } 94 | } 95 | 96 | // Submit the polygon vertices 97 | glBegin(GL_QUADS); 98 | glTexCoord2i(1, 1); 99 | glVertex2i(x + width, y + height); 100 | glTexCoord2i(0, 1); 101 | glVertex2i(x, y + height); 102 | glTexCoord2i(0, 0); 103 | glVertex2i(x, y); 104 | glTexCoord2i(1, 0); 105 | glVertex2i(x + width, y); 106 | glEnd(); 107 | } 108 | 109 | // Track the refresh rate and update the swap interval every second 110 | // Speed is limited by drawing, so this tries to keep it at 60 Hz 111 | refreshRate++; 112 | std::chrono::duration rateTime = std::chrono::steady_clock::now() - lastRateTime; 113 | if (rateTime.count() >= 1.0f) 114 | { 115 | swapInterval = (refreshRate + 5) / 60; // Margin of 5 116 | refreshRate = 0; 117 | lastRateTime = std::chrono::steady_clock::now(); 118 | } 119 | 120 | // Finish the frame 121 | glFinish(); 122 | SwapBuffers(); 123 | } 124 | 125 | void ryCanvas::resize(wxSizeEvent &event) 126 | { 127 | // Full screen breaks the minimum frame size, but changing to a different value fixes it 128 | // As a workaround, clear the minimum size on full screen and reset it shortly after 129 | frame->SetMinClientSize(sizeReset ? wxSize(0, 0) : MIN_SIZE); 130 | sizeReset -= (bool)sizeReset; 131 | 132 | // Update the canvas dimensions 133 | SetCurrent(*context); 134 | glMatrixMode(GL_PROJECTION); 135 | glLoadIdentity(); 136 | wxSize size = GetSize(); 137 | glOrtho(0, size.x, size.y, 0, -1, 1); 138 | glViewport(0, 0, size.x, size.y); 139 | 140 | // Set the layout to be centered and as large as possible 141 | if (((float)size.x / size.y) > (320.0f / 240)) // Wide 142 | { 143 | width = 320 * size.y / 240; 144 | height = size.y; 145 | x = (size.x - width) / 2; 146 | y = 0; 147 | } 148 | else // Tall 149 | { 150 | width = size.x; 151 | height = 240 * size.x / 320; 152 | x = 0; 153 | y = (size.y - height) / 2; 154 | } 155 | } 156 | 157 | void ryCanvas::pressKey(wxKeyEvent &event) 158 | { 159 | // Trigger a key press if a mapped key was pressed 160 | for (int i = 0; i < 19; i++) 161 | { 162 | if (event.GetKeyCode() == ryApp::keyBinds[i]) 163 | return frame->pressKey(i); 164 | } 165 | 166 | // Toggle full screen if the hotkey was pressed 167 | if (event.GetKeyCode() == ryApp::keyBinds[19]) 168 | { 169 | frame->ShowFullScreen(fullScreen = !fullScreen); 170 | sizeReset = 2; 171 | } 172 | } 173 | 174 | void ryCanvas::releaseKey(wxKeyEvent &event) 175 | { 176 | // Trigger a key release if a mapped key was released 177 | for (int i = 0; i < MAX_KEYS; i++) 178 | { 179 | if (event.GetKeyCode() == ryApp::keyBinds[i]) 180 | return frame->releaseKey(i); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/rsp_cp0.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "rsp_cp0.h" 21 | #include "log.h" 22 | #include "memory.h" 23 | #include "mi.h" 24 | #include "rdp.h" 25 | #include "rsp.h" 26 | 27 | namespace RSP_CP0 28 | { 29 | uint32_t memAddr; 30 | uint32_t dramAddr; 31 | uint32_t status; 32 | uint32_t semaphore; 33 | 34 | void performReadDma(uint32_t length, uint32_t count, uint32_t skip); 35 | void performWriteDma(uint32_t length, uint32_t count, uint32_t skip); 36 | } 37 | 38 | void RSP_CP0::reset() 39 | { 40 | // Reset the RSP CP0 to its initial state 41 | memAddr = 0; 42 | dramAddr = 0; 43 | status = 0x1; 44 | semaphore = 0; 45 | } 46 | 47 | uint32_t RSP_CP0::read(int index) 48 | { 49 | // Read from an RSP CP0 register if one exists at the given index 50 | switch (index) 51 | { 52 | case 4: // SP_STATUS 53 | // Get the status register 54 | return status; 55 | 56 | case 7: // SP_SEMAPHORE 57 | { 58 | // Set the semaphore to 1 and get the previous value 59 | uint32_t value = semaphore; 60 | semaphore = 1; 61 | return value; 62 | } 63 | 64 | case 8: case 9: case 10: case 11: 65 | case 12: case 13: case 14: case 15: 66 | // Get an RDP register 67 | return RDP::read(index - 8); 68 | 69 | default: 70 | LOG_WARN("Read from unknown RSP CP0 register: %d\n", index); 71 | return 0; 72 | } 73 | } 74 | 75 | void RSP_CP0::write(int index, uint32_t value) 76 | { 77 | // Write to an RSP CP0 register if one exists at the given index 78 | switch (index) 79 | { 80 | case 0: // SP_MEM_ADDR 81 | // Set the RSP DMA address 82 | memAddr = value & 0x1FF8; 83 | return; 84 | 85 | case 1: // SP_DRAM_ADDR 86 | // Set the RDRAM DMA address 87 | dramAddr = value & 0xFFFFF8; 88 | return; 89 | 90 | case 2: // SP_RD_LEN 91 | // Start a DMA transfer from RDRAM to RSP MEM 92 | performReadDma(value & 0xFF8, (value >> 12) & 0xFF, (value >> 20) & 0xFF8); 93 | return; 94 | 95 | case 3: // SP_WR_LEN 96 | // Start a DMA transfer from RSP MEM to RDRAM 97 | performWriteDma(value & 0xFF8, (value >> 12) & 0xFF, (value >> 20) & 0xFF8); 98 | return; 99 | 100 | case 4: // SP_STATUS 101 | // Set or clear the halt flag and update the RSP's state 102 | if (value & 0x1) 103 | status &= ~0x1; 104 | else if (value & 0x2) 105 | status |= 0x1; 106 | RSP::setState(status & 0x1); 107 | 108 | // Clear the broke flag 109 | if (value & 0x4) 110 | status &= ~0x2; 111 | 112 | // Acknowledge or trigger an SP interrupt 113 | if (value & 0x8) 114 | MI::clearInterrupt(0); 115 | else if (value & 0x10) 116 | MI::setInterrupt(0); 117 | 118 | // Set or clear the remaining status bits 119 | for (int i = 0; i < 20; i += 2) 120 | { 121 | if (value & (1 << (i + 5))) 122 | status &= ~(1 << ((i / 2) + 5)); 123 | else if (value & (1 << (i + 6))) 124 | status |= (1 << ((i / 2) + 5)); 125 | } 126 | 127 | // Keep track of unimplemented bits that should do something 128 | if (uint32_t bits = (status & 0x20)) 129 | LOG_WARN("Unimplemented RSP CP0 status bits set: 0x%X\n", bits); 130 | return; 131 | 132 | case 7: // SP_SEMAPHORE 133 | // Set the semaphore value 134 | semaphore = value & 0x1; 135 | return; 136 | 137 | case 8: case 9: case 10: case 11: 138 | case 12: case 13: case 14: case 15: 139 | // Set an RDP register 140 | return RDP::write(index - 8, value); 141 | 142 | default: 143 | LOG_WARN("Write to unknown RSP CP0 register: %d\n", index); 144 | return; 145 | } 146 | } 147 | 148 | void RSP_CP0::triggerBreak() 149 | { 150 | // Trigger an SP interrupt if enabled, halt the RSP, and set the broke flag 151 | if (status & 0x40) 152 | MI::setInterrupt(0); 153 | RSP::setState(true); 154 | status |= 0x3; 155 | } 156 | 157 | void RSP_CP0::performReadDma(uint32_t length, uint32_t count, uint32_t skip) 158 | { 159 | LOG_INFO("RSP DMA from RDRAM 0x%X to RSP MEM 0x%X with length 0x%X, " 160 | "count 0x%X, skip 0x%X\n", dramAddr, memAddr, length, count, skip); 161 | 162 | // Copy rows of data from memory to the RSP 163 | uint32_t dramBase = dramAddr, memBase = memAddr; 164 | for (uint32_t c = 0; c <= count; c++) 165 | { 166 | for (uint32_t l = 0; l <= length; l += 8) 167 | { 168 | uint32_t dst = 0x84000000 + ((memBase + l) & 0x1FF8); 169 | uint32_t src = 0x80000000 + ((dramBase + l) & 0xFFFFF8); 170 | Memory::write(dst, Memory::read(src)); 171 | } 172 | dramBase += length + skip + 8; 173 | memBase += length + 8; 174 | } 175 | } 176 | 177 | void RSP_CP0::performWriteDma(uint32_t length, uint32_t count, uint32_t skip) 178 | { 179 | LOG_INFO("RSP DMA from RSP MEM 0x%X to RDRAM 0x%X with length 0x%X, " 180 | "count 0x%X, skip 0x%X\n", memAddr, dramAddr, length, count, skip); 181 | 182 | // Copy rows of data from the RSP to memory 183 | uint32_t dramBase = dramAddr, memBase = memAddr; 184 | for (uint32_t c = 0; c <= count; c++) 185 | { 186 | for (uint32_t l = 0; l <= length; l += 8) 187 | { 188 | uint32_t dst = 0x80000000 + ((dramBase + l) & 0xFFFFF8); 189 | uint32_t src = 0x84000000 + ((memBase + l) & 0x1FF8); 190 | Memory::write(dst, Memory::read(src)); 191 | } 192 | dramBase += length + skip + 8; 193 | memBase += length + 8; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/vi.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include "vi.h" 27 | #include "core.h" 28 | #include "log.h" 29 | #include "memory.h" 30 | #include "mi.h" 31 | #include "rdp.h" 32 | 33 | namespace VI 34 | { 35 | std::queue<_Framebuffer*> framebuffers; 36 | std::atomic ready; 37 | std::mutex mutex; 38 | 39 | uint32_t control; 40 | uint32_t origin; 41 | uint32_t width; 42 | uint32_t hVideo; 43 | uint32_t vVideo; 44 | uint32_t xScale; 45 | uint32_t yScale; 46 | 47 | void drawFrame(); 48 | } 49 | 50 | _Framebuffer *VI::getFramebuffer() 51 | { 52 | // Wait until a new frame is ready 53 | if (!ready.load()) 54 | return nullptr; 55 | 56 | // Get the next frame in the queue 57 | mutex.lock(); 58 | _Framebuffer *fb = framebuffers.front(); 59 | framebuffers.pop(); 60 | ready.store(!framebuffers.empty()); 61 | mutex.unlock(); 62 | return fb; 63 | } 64 | 65 | void VI::reset() 66 | { 67 | // Reset the VI to its initial state 68 | control = 0; 69 | origin = 0; 70 | width = 0; 71 | hVideo = 0; 72 | vVideo = 0; 73 | xScale = 0; 74 | yScale = 0; 75 | 76 | // Schedule the first frame to be drawn 77 | Core::schedule(drawFrame, (93750000 / 60) * 2); 78 | } 79 | 80 | uint32_t VI::read(uint32_t address) 81 | { 82 | // Read from an I/O register if one exists at the given address 83 | switch (address) 84 | { 85 | default: 86 | LOG_WARN("Unknown VI register read: 0x%X\n", address); 87 | return 0; 88 | } 89 | } 90 | 91 | void VI::write(uint32_t address, uint32_t value) 92 | { 93 | // Write to an I/O register if one exists at the given address 94 | switch (address) 95 | { 96 | case 0x4400000: // VI_CONTROL 97 | // Set the VI control register 98 | // TODO: actually use bits other than type 99 | control = (value & 0x1FBFF); 100 | return; 101 | 102 | case 0x4400004: // VI_ORIGIN 103 | // Set the framebuffer address 104 | origin = 0x80000000 | (value & 0xFFFFFF); 105 | return; 106 | 107 | case 0x4400008: // VI_WIDTH 108 | // Set the framebuffer width in pixels 109 | width = (value & 0xFFF); 110 | return; 111 | 112 | case 0x4400010: // VI_V_CURRENT 113 | // Acknowledge a VI interrupt instead of writing a value 114 | MI::clearInterrupt(3); 115 | return; 116 | 117 | case 0x4400024: // VI_H_VIDEO 118 | { 119 | // Set the range of visible horizontal pixels 120 | uint32_t start = (value >> 16) & 0x3FF; 121 | uint32_t end = (value >> 0) & 0x3FF; 122 | hVideo = (end - start); 123 | return; 124 | } 125 | 126 | case 0x4400028: // VI_V_VIDEO 127 | { 128 | // Set the range of visible vertical pixels 129 | uint32_t start = (value >> 16) & 0x3FF; 130 | uint32_t end = (value >> 0) & 0x3FF; 131 | vVideo = (end - start) / 2; 132 | return; 133 | } 134 | 135 | case 0x4400030: // VI_X_SCALE 136 | // Set the framebuffer X-scale 137 | // TODO: actually use offset value 138 | xScale = (value & 0xFFF); 139 | return; 140 | 141 | case 0x4400034: // VI_Y_SCALE 142 | // Set the framebuffer Y-scale 143 | // TODO: actually use offset value 144 | yScale = (value & 0xFFF); 145 | return; 146 | 147 | default: 148 | LOG_WARN("Unknown VI register write: 0x%X\n", address); 149 | return; 150 | } 151 | } 152 | 153 | void VI::drawFrame() 154 | { 155 | // Ensure the RDP thread has finished drawing 156 | RDP::finishThread(); 157 | 158 | // Allow up to 2 framebuffers to be queued, to preserve frame pacing if emulation runs ahead 159 | if (framebuffers.size() < 2) 160 | { 161 | // Create a new framebuffer 162 | _Framebuffer *fb = new _Framebuffer(); 163 | fb->width = ((xScale ? xScale : 0x200) * hVideo) >> 10; 164 | fb->height = ((yScale ? yScale : 0x200) * vVideo) >> 10; 165 | fb->data = new uint32_t[fb->width * fb->height]; 166 | 167 | // Clear the screen if there's nothing to display 168 | if (fb->width == 0 || fb->height == 0) 169 | { 170 | fb->width = 8; 171 | fb->height = 8; 172 | delete[] fb->data; 173 | fb->data = new uint32_t[fb->width * fb->height]; 174 | goto clear; 175 | } 176 | 177 | // Read the framebuffer from N64 memory 178 | switch (control & 0x3) // Type 179 | { 180 | case 0x3: // 32-bit 181 | // Translate pixels from RGB_8888 to ARGB8888 182 | for (uint32_t y = 0; y < fb->height; y++) 183 | { 184 | for (uint32_t x = 0; x < fb->width; x++) 185 | { 186 | uint32_t color = Memory::read(origin + ((y * width + x) << 2)); 187 | uint8_t r = (color >> 24) & 0xFF; 188 | uint8_t g = (color >> 16) & 0xFF; 189 | uint8_t b = (color >> 8) & 0xFF; 190 | fb->data[y * fb->width + x] = (0xFF << 24) | (b << 16) | (g << 8) | r; 191 | } 192 | } 193 | break; 194 | 195 | case 0x2: // 16-bit 196 | // Translate pixels from RGB_5551 to ARGB8888 197 | for (uint32_t y = 0; y < fb->height; y++) 198 | { 199 | for (uint32_t x = 0; x < fb->width; x++) 200 | { 201 | uint16_t color = Memory::read(origin + ((y * width + x) << 1)); 202 | uint8_t r = ((color >> 11) & 0x1F) * 255 / 31; 203 | uint8_t g = ((color >> 6) & 0x1F) * 255 / 31; 204 | uint8_t b = ((color >> 1) & 0x1F) * 255 / 31; 205 | fb->data[y * fb->width + x] = (0xFF << 24) | (b << 16) | (g << 8) | r; 206 | } 207 | } 208 | break; 209 | 210 | default: 211 | clear: 212 | // Don't show anything 213 | memset(fb->data, 0, fb->width * fb->height * sizeof(uint32_t)); 214 | break; 215 | } 216 | 217 | // Add the frame to the queue 218 | mutex.lock(); 219 | framebuffers.push(fb); 220 | ready.store(true); 221 | mutex.unlock(); 222 | } 223 | 224 | // Finish the frame and request a VI interrupt 225 | // TODO: request interrupt at the proper time 226 | MI::setInterrupt(3); 227 | 228 | // Schedule the next frame to be drawn 229 | Core::schedule(drawFrame, (93750000 / 60) * 2); 230 | Core::countFrame(); 231 | } 232 | -------------------------------------------------------------------------------- /src/ai.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include "ai.h" 27 | #include "core.h" 28 | #include "log.h" 29 | #include "memory.h" 30 | #include "mi.h" 31 | #include "settings.h" 32 | 33 | #define MAX_BUFFERS 4 34 | #define SAMPLE_COUNT 1024 35 | #define OUTPUT_RATE 48000 36 | #define OUTPUT_SIZE SAMPLE_COUNT * sizeof(uint32_t) 37 | 38 | struct Samples 39 | { 40 | uint32_t address; 41 | uint32_t count; 42 | }; 43 | 44 | namespace AI 45 | { 46 | uint32_t bufferOut[SAMPLE_COUNT]; 47 | std::atomic ready; 48 | 49 | Samples samples[2]; 50 | std::queue> buffers; 51 | uint32_t offset; 52 | 53 | uint32_t dramAddr; 54 | uint32_t control; 55 | uint32_t frequency; 56 | uint32_t status; 57 | 58 | void createBuffer(); 59 | void submitBuffer(); 60 | void processBuffer(); 61 | } 62 | 63 | void AI::fillBuffer(uint32_t *out) 64 | { 65 | // Try to wait until a buffer is ready, but don't stall the audio callback too long 66 | std::chrono::steady_clock::time_point waitTime = std::chrono::steady_clock::now(); 67 | while (!ready.load()) 68 | { 69 | if (std::chrono::steady_clock::now() - waitTime > std::chrono::microseconds(1000000 / 60)) 70 | { 71 | // If a buffer isn't ready in time, fill the output with the last played sample 72 | for (int i = 0; i < SAMPLE_COUNT; i++) 73 | out[i] = bufferOut[SAMPLE_COUNT - 1]; 74 | return; 75 | } 76 | } 77 | 78 | // Output the buffer and mark it as used 79 | memcpy(out, bufferOut, OUTPUT_SIZE); 80 | ready.store(false); 81 | } 82 | 83 | void AI::reset() 84 | { 85 | // Reset the AI to its initial state 86 | dramAddr = 0; 87 | control = 0; 88 | frequency = 0; 89 | status = 0; 90 | 91 | // Schedule the first audio buffer to output 92 | Core::schedule(createBuffer, (uint64_t)SAMPLE_COUNT * (93750000 * 2) / OUTPUT_RATE); 93 | } 94 | 95 | uint32_t AI::read(uint32_t address) 96 | { 97 | // Read from an I/O register if one exists at the given address 98 | switch (address) 99 | { 100 | case 0x450000C: // AI_STATUS 101 | // Get the status register 102 | return status; 103 | 104 | default: 105 | LOG_WARN("Unknown AI register read: 0x%X\n", address); 106 | return 0; 107 | } 108 | } 109 | 110 | void AI::write(uint32_t address, uint32_t value) 111 | { 112 | // Write to an I/O register if one exists at the given address 113 | switch (address) 114 | { 115 | case 0x4500000: // AI_DRAM_ADDR 116 | // Set the RDRAM DMA address 117 | dramAddr = value & 0xFFFFFF; 118 | return; 119 | 120 | case 0x4500004: // AI_LENGTH 121 | if (control) // DMA enabled 122 | { 123 | if (status & (1 << 30)) // Busy 124 | { 125 | // Queue a second set of samples while the first is processed 126 | status |= (1 << 31); // Full 127 | samples[1].address = dramAddr; 128 | samples[1].count = (value & ~0x7) / 4; 129 | } 130 | else 131 | { 132 | // Queue a set of samples and submit them as an audio buffer 133 | status |= (1 << 30); // Busy 134 | samples[0].address = dramAddr; 135 | samples[0].count = (value & ~0x7) / 4; 136 | submitBuffer(); 137 | } 138 | } 139 | return; 140 | 141 | case 0x4500008: // AI_CONTROL 142 | // Set the control register 143 | control = value & 0x1; 144 | return; 145 | 146 | case 0x450000C: // AI_STATUS 147 | // Acknowledge an AI interrupt 148 | MI::clearInterrupt(2); 149 | return; 150 | 151 | case 0x4500010: // AI_DAC_RATE 152 | // Set the audio frequency based on the NTSC DAC rate 153 | frequency = 48681812 / (value & 0x3FFF); 154 | return; 155 | 156 | default: 157 | LOG_WARN("Unknown AI register write: 0x%X\n", address); 158 | return; 159 | } 160 | } 161 | 162 | void AI::createBuffer() 163 | { 164 | // Wait until the previous buffer has been used 165 | while (Settings::fpsLimiter && Core::running && ready.load()) 166 | std::this_thread::yield(); 167 | 168 | memset(bufferOut, 0, OUTPUT_SIZE); 169 | size_t count = 0; 170 | 171 | while (!buffers.empty() && count < OUTPUT_SIZE) 172 | { 173 | // Get the current queued buffer and the size of its remaining samples 174 | std::vector &buffer = buffers.front(); 175 | size_t size = (buffer.size() - offset) * sizeof(uint32_t); 176 | 177 | if (size <= OUTPUT_SIZE - count) 178 | { 179 | // Copy all of the remaining queued samples to the output buffer 180 | memcpy(&bufferOut[count / sizeof(uint32_t)], &buffer[offset], size); 181 | count += size; 182 | offset = 0; 183 | buffers.pop(); 184 | } 185 | else 186 | { 187 | // Copy as many queued samples that can fit to the output buffer 188 | memcpy(&bufferOut[count / sizeof(uint32_t)], &buffer[offset], OUTPUT_SIZE - count); 189 | offset += (OUTPUT_SIZE - count) / sizeof(uint32_t); 190 | break; 191 | } 192 | } 193 | 194 | // Mark the buffer as ready and schedule the next one 195 | ready.store(true); 196 | Core::schedule(createBuffer, (uint64_t)SAMPLE_COUNT * (93750000 * 2) / OUTPUT_RATE); 197 | } 198 | 199 | void AI::submitBuffer() 200 | { 201 | LOG_INFO("Submitting %d AI samples from RDRAM 0x%X at frequency %dHz\n", 202 | samples[0].count, samples[0].address, frequency); 203 | 204 | if (buffers.size() < MAX_BUFFERS) 205 | { 206 | // Create a new audio buffer based on sample count and frequency 207 | size_t count = samples[0].count * OUTPUT_RATE / frequency; 208 | std::vector buffer(count); 209 | 210 | // Copy samples to the buffer, scaled from their original frequency 211 | for (size_t i = 0; i < count; i++) 212 | { 213 | uint32_t address = samples[0].address + (i * samples[0].count / count) * 4; 214 | uint32_t value = Memory::read(0xA0000000 + address); 215 | buffer[i] = (value << 16) | (value >> 16); 216 | } 217 | 218 | // Add the buffer to the output queue 219 | buffers.push(buffer); 220 | } 221 | 222 | // Schedule the logical completion of the AI DMA based on sample count and frequency 223 | Core::schedule(processBuffer, (uint64_t)samples[0].count * (93750000 * 2) / frequency); 224 | } 225 | 226 | void AI::processBuffer() 227 | { 228 | if (status & (1 << 31)) // Full 229 | { 230 | // Submit the next queued samples and trigger an AI interrupt to request more 231 | status &= ~(1 << 31); // Not full 232 | samples[0] = samples[1]; 233 | submitBuffer(); 234 | MI::setInterrupt(2); 235 | } 236 | else 237 | { 238 | // Stop running because there are no more samples to submit 239 | status &= ~(1 << 30); // Not busy 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Makefile.switch: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | .SUFFIXES: 3 | #--------------------------------------------------------------------------------- 4 | 5 | ifeq ($(strip $(DEVKITPRO)),) 6 | $(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") 7 | endif 8 | 9 | TOPDIR ?= $(CURDIR) 10 | include $(DEVKITPRO)/libnx/switch_rules 11 | 12 | #--------------------------------------------------------------------------------- 13 | # TARGET is the name of the output 14 | # BUILD is the directory where object files & intermediate files will be placed 15 | # SOURCES is a list of directories containing source code 16 | # DATA is a list of directories containing data files 17 | # INCLUDES is a list of directories containing header files 18 | # ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) 19 | # 20 | # NO_ICON: if set to anything, do not use icon. 21 | # NO_NACP: if set to anything, no .nacp file is generated. 22 | # APP_TITLE is the name of the app stored in the .nacp file (Optional) 23 | # APP_AUTHOR is the author of the app stored in the .nacp file (Optional) 24 | # APP_VERSION is the version of the app stored in the .nacp file (Optional) 25 | # APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) 26 | # ICON is the filename of the icon (.jpg), relative to the project folder. 27 | # If not set, it attempts to use one of the following (in this order): 28 | # - .jpg 29 | # - icon.jpg 30 | # - /default_icon.jpg 31 | # 32 | # CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. 33 | # If not set, it attempts to use one of the following (in this order): 34 | # - .json 35 | # - config.json 36 | # If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead 37 | # of a homebrew executable (.nro). This is intended to be used for sysmodules. 38 | # NACP building is skipped as well. 39 | #--------------------------------------------------------------------------------- 40 | TARGET := rokuyon 41 | BUILD := build-switch 42 | SOURCES := src src/switch 43 | DATA := data 44 | INCLUDES := src src/switch 45 | ROMFS := romfs 46 | 47 | APP_TITLE := rokuyon 48 | APP_AUTHOR := Hydr8gon 49 | APP_VERSION := 0.1 50 | 51 | #--------------------------------------------------------------------------------- 52 | # options for code generation 53 | #--------------------------------------------------------------------------------- 54 | ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE 55 | 56 | CFLAGS := -g -Wall -O3 -flto -ffunction-sections \ 57 | $(ARCH) $(DEFINES) 58 | 59 | CFLAGS += $(INCLUDE) -D__SWITCH__ -DLOG_LEVEL=0 60 | 61 | CXXFLAGS := $(CFLAGS) -fno-rtti -std=c++11 62 | 63 | ASFLAGS := -g $(ARCH) 64 | LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) 65 | 66 | LIBS := -lglad -lEGL -lglapi -ldrm_nouveau -lnx 67 | 68 | #--------------------------------------------------------------------------------- 69 | # list of directories containing libraries, this must be the top level containing 70 | # include and lib 71 | #--------------------------------------------------------------------------------- 72 | LIBDIRS := $(PORTLIBS) $(LIBNX) 73 | 74 | 75 | #--------------------------------------------------------------------------------- 76 | # no real need to edit anything past this point unless you need to add additional 77 | # rules for different file extensions 78 | #--------------------------------------------------------------------------------- 79 | ifneq ($(BUILD),$(notdir $(CURDIR))) 80 | #--------------------------------------------------------------------------------- 81 | 82 | export OUTPUT := $(CURDIR)/$(TARGET) 83 | export TOPDIR := $(CURDIR) 84 | 85 | export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ 86 | $(foreach dir,$(DATA),$(CURDIR)/$(dir)) 87 | 88 | export DEPSDIR := $(CURDIR)/$(BUILD) 89 | 90 | CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) 91 | CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) 92 | SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) 93 | BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) 94 | 95 | #--------------------------------------------------------------------------------- 96 | # use CXX for linking C++ projects, CC for standard C 97 | #--------------------------------------------------------------------------------- 98 | ifeq ($(strip $(CPPFILES)),) 99 | #--------------------------------------------------------------------------------- 100 | export LD := $(CC) 101 | #--------------------------------------------------------------------------------- 102 | else 103 | #--------------------------------------------------------------------------------- 104 | export LD := $(CXX) 105 | #--------------------------------------------------------------------------------- 106 | endif 107 | #--------------------------------------------------------------------------------- 108 | 109 | export OFILES_BIN := $(addsuffix .o,$(BINFILES)) 110 | export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) 111 | export OFILES := $(OFILES_BIN) $(OFILES_SRC) 112 | export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) 113 | 114 | export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ 115 | $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ 116 | -I$(CURDIR)/$(BUILD) 117 | 118 | export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) 119 | 120 | ifeq ($(strip $(CONFIG_JSON)),) 121 | jsons := $(wildcard *.json) 122 | ifneq (,$(findstring $(TARGET).json,$(jsons))) 123 | export APP_JSON := $(TOPDIR)/$(TARGET).json 124 | else 125 | ifneq (,$(findstring config.json,$(jsons))) 126 | export APP_JSON := $(TOPDIR)/config.json 127 | endif 128 | endif 129 | else 130 | export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) 131 | endif 132 | 133 | ifeq ($(strip $(ICON)),) 134 | icons := $(wildcard *.jpg) 135 | ifneq (,$(findstring $(TARGET).jpg,$(icons))) 136 | export APP_ICON := $(TOPDIR)/$(TARGET).jpg 137 | else 138 | ifneq (,$(findstring icon.jpg,$(icons))) 139 | export APP_ICON := $(TOPDIR)/icon.jpg 140 | endif 141 | endif 142 | else 143 | export APP_ICON := $(TOPDIR)/$(ICON) 144 | endif 145 | 146 | ifeq ($(strip $(NO_ICON)),) 147 | export NROFLAGS += --icon=$(APP_ICON) 148 | endif 149 | 150 | ifeq ($(strip $(NO_NACP)),) 151 | export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp 152 | endif 153 | 154 | ifneq ($(APP_TITLEID),) 155 | export NACPFLAGS += --titleid=$(APP_TITLEID) 156 | endif 157 | 158 | ifneq ($(ROMFS),) 159 | export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) 160 | endif 161 | 162 | .PHONY: $(BUILD) clean all 163 | 164 | #--------------------------------------------------------------------------------- 165 | all: $(BUILD) 166 | 167 | $(BUILD): 168 | @[ -d $@ ] || mkdir -p $@ 169 | @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.switch 170 | 171 | #--------------------------------------------------------------------------------- 172 | clean: 173 | @echo clean ... 174 | ifeq ($(strip $(APP_JSON)),) 175 | @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf 176 | else 177 | @rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf 178 | endif 179 | 180 | 181 | #--------------------------------------------------------------------------------- 182 | else 183 | .PHONY: all 184 | 185 | DEPENDS := $(OFILES:.o=.d) 186 | 187 | #--------------------------------------------------------------------------------- 188 | # main targets 189 | #--------------------------------------------------------------------------------- 190 | ifeq ($(strip $(APP_JSON)),) 191 | 192 | all : $(OUTPUT).nro 193 | 194 | ifeq ($(strip $(NO_NACP)),) 195 | $(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp 196 | else 197 | $(OUTPUT).nro : $(OUTPUT).elf 198 | endif 199 | 200 | else 201 | 202 | all : $(OUTPUT).nsp 203 | 204 | $(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm 205 | 206 | $(OUTPUT).nso : $(OUTPUT).elf 207 | 208 | endif 209 | 210 | $(OUTPUT).elf : $(OFILES) 211 | 212 | $(OFILES_SRC) : $(HFILES_BIN) 213 | 214 | #--------------------------------------------------------------------------------- 215 | # you need a rule like this for each extension you use as binary data 216 | #--------------------------------------------------------------------------------- 217 | %.bin.o %_bin.h : %.bin 218 | #--------------------------------------------------------------------------------- 219 | @echo $(notdir $<) 220 | @$(bin2o) 221 | 222 | -include $(DEPENDS) 223 | 224 | #--------------------------------------------------------------------------------------- 225 | endif 226 | #--------------------------------------------------------------------------------------- 227 | -------------------------------------------------------------------------------- /src/core.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "core.h" 28 | #include "ai.h" 29 | #include "cpu.h" 30 | #include "cpu_cp0.h" 31 | #include "cpu_cp1.h" 32 | #include "log.h" 33 | #include "memory.h" 34 | #include "mi.h" 35 | #include "pi.h" 36 | #include "pif.h" 37 | #include "rdp.h" 38 | #include "rsp.h" 39 | #include "rsp_cp0.h" 40 | #include "rsp_cp2.h" 41 | #include "si.h" 42 | #include "vi.h" 43 | 44 | struct Task 45 | { 46 | Task(void (*function)(), uint32_t cycles): 47 | function(function), cycles(cycles) {} 48 | 49 | void (*function)(); 50 | uint32_t cycles; 51 | 52 | bool operator<(const Task &task) const 53 | { 54 | return cycles < task.cycles; 55 | } 56 | }; 57 | 58 | namespace Core 59 | { 60 | std::thread *emuThread; 61 | std::thread *saveThread; 62 | std::condition_variable condVar; 63 | std::mutex waitMutex; 64 | std::mutex saveMutex; 65 | 66 | bool running; 67 | bool cpuRunning; 68 | bool rspRunning; 69 | 70 | std::vector tasks; 71 | uint32_t globalCycles; 72 | uint32_t cpuCycles; 73 | uint32_t rspCycles; 74 | 75 | int fps; 76 | int fpsCount; 77 | std::chrono::steady_clock::time_point lastFpsTime; 78 | 79 | std::string savePath; 80 | uint8_t *rom; 81 | uint8_t *save; 82 | uint32_t romSize; 83 | uint32_t saveSize; 84 | bool saveDirty; 85 | 86 | void runLoop(); 87 | void saveLoop(); 88 | void updateSave(); 89 | void resetCycles(); 90 | } 91 | 92 | bool Core::bootRom(const std::string &path) 93 | { 94 | // Try to open the specified ROM file 95 | FILE *romFile = fopen(path.c_str(), "rb"); 96 | if (!romFile) return false; 97 | 98 | // Ensure the emulator is stopped 99 | stop(); 100 | 101 | // Load the ROM into memory 102 | if (rom) delete[] rom; 103 | fseek(romFile, 0, SEEK_END); 104 | romSize = ftell(romFile); 105 | fseek(romFile, 0, SEEK_SET); 106 | rom = new uint8_t[romSize]; 107 | fread(rom, sizeof(uint8_t), romSize, romFile); 108 | fclose(romFile); 109 | 110 | // Derive the save path from the ROM path 111 | savePath = path.substr(0, path.rfind(".")) + ".sav"; 112 | if (save) delete[] save; 113 | saveDirty = false; 114 | 115 | if (FILE *saveFile = fopen(savePath.c_str(), "rb")) 116 | { 117 | // Load the save file into memory if it exists 118 | fseek(saveFile, 0, SEEK_END); 119 | saveSize = ftell(saveFile); 120 | fseek(saveFile, 0, SEEK_SET); 121 | save = new uint8_t[saveSize]; 122 | fread(save, sizeof(uint8_t), saveSize, saveFile); 123 | fclose(saveFile); 124 | } 125 | else 126 | { 127 | // If no save file exists, assume no save 128 | // TODO: some sort of detection, or a database 129 | saveSize = 0; 130 | save = nullptr; 131 | } 132 | 133 | // Reset the scheduler 134 | cpuRunning = true; 135 | tasks.clear(); 136 | globalCycles = 0; 137 | cpuCycles = 0; 138 | rspCycles = 0; 139 | schedule(resetCycles, 0x7FFFFFFF); 140 | 141 | // Reset the emulated components 142 | Memory::reset(); 143 | AI::reset(); 144 | CPU::reset(); 145 | CPU_CP0::reset(); 146 | CPU_CP1::reset(); 147 | MI::reset(); 148 | PI::reset(); 149 | SI::reset(); 150 | VI::reset(); 151 | PIF::reset(); 152 | RDP::reset(); 153 | RSP::reset(); 154 | RSP_CP0::reset(); 155 | RSP_CP2::reset(); 156 | 157 | // Start the emulator 158 | start(); 159 | return true; 160 | } 161 | 162 | void Core::resizeSave(uint32_t newSize) 163 | { 164 | // Create a save with the new size 165 | saveMutex.lock(); 166 | uint8_t *newSave = new uint8_t[newSize]; 167 | 168 | if (saveSize < newSize) // New save is larger 169 | { 170 | // Copy all of the old save and fill the rest with 0xFF 171 | memcpy(newSave, save, saveSize * sizeof(uint8_t)); 172 | memset(&newSave[saveSize], 0xFF, (newSize - saveSize) * sizeof(uint8_t)); 173 | } 174 | else // New save is smaller 175 | { 176 | // Copy as much of the old save as possible 177 | memcpy(newSave, save, newSize * sizeof(uint8_t)); 178 | } 179 | 180 | // Swap the old save for the new one 181 | delete[] save; 182 | save = newSave; 183 | saveSize = newSize; 184 | saveDirty = true; 185 | saveMutex.unlock(); 186 | updateSave(); 187 | } 188 | 189 | void Core::start() 190 | { 191 | // Start the threads if emulation wasn't running 192 | if (!running) 193 | { 194 | running = true; 195 | emuThread = new std::thread(runLoop); 196 | saveThread = new std::thread(saveLoop); 197 | } 198 | } 199 | 200 | void Core::stop() 201 | { 202 | if (running) 203 | { 204 | { 205 | // Signal for the threads to stop 206 | std::lock_guard guard(waitMutex); 207 | running = false; 208 | condVar.notify_one(); 209 | } 210 | 211 | // Stop the threads if emulation was running 212 | emuThread->join(); 213 | saveThread->join(); 214 | delete emuThread; 215 | delete saveThread; 216 | RDP::finishThread(); 217 | } 218 | } 219 | 220 | void Core::runLoop() 221 | { 222 | while (running) 223 | { 224 | // Run the CPUs until the next scheduled task 225 | while (tasks[0].cycles > globalCycles) 226 | { 227 | // Run a CPU opcode if ready and schedule the next one 228 | if (cpuRunning && globalCycles >= cpuCycles) 229 | { 230 | CPU::runOpcode(); 231 | cpuCycles = globalCycles + 2; 232 | } 233 | 234 | // Run an RSP opcode if ready and schedule the next one 235 | if (rspRunning && globalCycles >= rspCycles) 236 | { 237 | RSP::runOpcode(); 238 | rspCycles = globalCycles + 3; 239 | } 240 | 241 | // Jump to the next soonest opcode 242 | globalCycles = std::min(cpuRunning ? cpuCycles : -1, rspRunning ? rspCycles : -1); 243 | } 244 | 245 | // Jump to the next scheduled task 246 | globalCycles = tasks[0].cycles; 247 | 248 | // Run all tasks that are scheduled now 249 | while (tasks[0].cycles <= globalCycles) 250 | { 251 | (*tasks[0].function)(); 252 | tasks.erase(tasks.begin()); 253 | } 254 | } 255 | } 256 | 257 | void Core::saveLoop() 258 | { 259 | while (running) 260 | { 261 | // Every few seconds, check if the save file should be updated 262 | std::unique_lock lock(waitMutex); 263 | condVar.wait_for(lock, std::chrono::seconds(3), [&]{ return !running; }); 264 | updateSave(); 265 | } 266 | } 267 | 268 | void Core::countFrame() 269 | { 270 | // Calculate the time since the FPS was last updated 271 | std::chrono::duration fpsTime = std::chrono::steady_clock::now() - lastFpsTime; 272 | 273 | if (fpsTime.count() >= 1.0f) 274 | { 275 | // Update the FPS value after one second and reset the counter 276 | fps = fpsCount; 277 | fpsCount = 0; 278 | lastFpsTime = std::chrono::steady_clock::now(); 279 | } 280 | else 281 | { 282 | // Count another frame 283 | fpsCount++; 284 | } 285 | } 286 | 287 | void Core::writeSave(uint32_t address, uint8_t value) 288 | { 289 | // Safely write a byte of data to the current save 290 | saveMutex.lock(); 291 | save[address] = value; 292 | saveDirty = true; 293 | saveMutex.unlock(); 294 | } 295 | 296 | void Core::updateSave() 297 | { 298 | // Update the save file if the data changed 299 | saveMutex.lock(); 300 | if (saveDirty) 301 | { 302 | if (FILE *saveFile = fopen(savePath.c_str(), "wb")) 303 | { 304 | LOG_INFO("Writing save file to disk\n"); 305 | fwrite(save, sizeof(uint8_t), saveSize, saveFile); 306 | fclose(saveFile); 307 | saveDirty = false; 308 | } 309 | } 310 | saveMutex.unlock(); 311 | } 312 | 313 | void Core::resetCycles() 314 | { 315 | // Reset the cycle counts to prevent overflow 316 | CPU_CP0::resetCycles(); 317 | for (size_t i = 0; i < tasks.size(); i++) 318 | tasks[i].cycles -= globalCycles; 319 | cpuCycles -= std::min(globalCycles, cpuCycles); 320 | rspCycles -= std::min(globalCycles, rspCycles); 321 | globalCycles -= globalCycles; 322 | 323 | // Schedule the next cycle reset 324 | schedule(resetCycles, 0x7FFFFFFF); 325 | } 326 | 327 | void Core::schedule(void (*function)(), uint32_t cycles) 328 | { 329 | // Add a task to the scheduler, sorted by least to most cycles until execution 330 | // Cycles run at 93.75 * 2 MHz 331 | Task task(function, globalCycles + cycles); 332 | auto it = std::upper_bound(tasks.cbegin(), tasks.cend(), task); 333 | tasks.insert(it, task); 334 | } 335 | -------------------------------------------------------------------------------- /src/pif.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | 22 | #include "pif.h" 23 | #include "core.h" 24 | #include "cpu.h" 25 | #include "log.h" 26 | #include "memory.h" 27 | #include "settings.h" 28 | 29 | namespace PIF 30 | { 31 | uint8_t memory[0x800]; // 2KB-64B PIF ROM + 64B PIF RAM 32 | uint16_t eepromMask; 33 | uint8_t eepromId; 34 | 35 | uint8_t command; 36 | uint16_t buttons; 37 | int8_t stickX; 38 | int8_t stickY; 39 | 40 | extern void (*pifCommands[])(int); 41 | 42 | uint32_t crc32(uint8_t *data, size_t size); 43 | void joybusProtocol(int bit); 44 | void verifyChecksum(int bit); 45 | void clearMemory(int bit); 46 | void unknownCmd(int bit); 47 | } 48 | 49 | // Small command lookup table for PIF command bits 50 | void (*PIF::pifCommands[7])(int) = 51 | { 52 | joybusProtocol, unknownCmd, unknownCmd, unknownCmd, // 0-3 53 | unknownCmd, verifyChecksum, clearMemory // 4-6 54 | }; 55 | 56 | uint32_t PIF::crc32(uint8_t *data, size_t size) 57 | { 58 | uint32_t r = 0xFFFFFFFF; 59 | 60 | // Calculate a CRC32 value for the given data 61 | for (size_t i = 0; i < size; i++) 62 | { 63 | r ^= data[i]; 64 | for (int j = 0; j < 8; j++) 65 | { 66 | uint32_t t = ~((r & 1) - 1); 67 | r = (r >> 1) ^ (0xEDB88320 & t); 68 | } 69 | } 70 | 71 | return ~r; 72 | } 73 | 74 | void PIF::reset() 75 | { 76 | // Reset the PIF to its initial state 77 | clearMemory(0); 78 | command = 0; 79 | buttons = 0; 80 | stickX = 0; 81 | stickY = 0; 82 | 83 | // Set a mask and ID for 0.5KB/2KB EEPROM, or disable EEPROM 84 | switch (Core::saveSize) 85 | { 86 | case 0x200: eepromMask = 0x1FF; eepromId = 0x80; break; 87 | case 0x800: eepromMask = 0x7FF; eepromId = 0xC0; break; 88 | default: eepromMask = 0x000; eepromId = 0x00; break; 89 | } 90 | 91 | // Set the CIC seed based on which bootcode is detected 92 | // This value is used during boot to calculate a checksum 93 | switch (uint32_t value = crc32(&Core::rom[0x40], 0x1000 - 0x40)) 94 | { 95 | case 0x6170A4A1: // 6101 96 | LOG_INFO("Detected CIC chip 6101\n"); 97 | Memory::write(0xBFC007E6, 0x3F); 98 | break; 99 | 100 | case 0x90BB6CB5: // 6102 101 | LOG_INFO("Detected CIC chip 6102\n"); 102 | Memory::write(0xBFC007E6, 0x3F); 103 | break; 104 | 105 | case 0x0B050EE0: // 6103 106 | LOG_INFO("Detected CIC chip 6103\n"); 107 | Memory::write(0xBFC007E6, 0x78); 108 | break; 109 | 110 | case 0x98BC2C86: // 6105 111 | LOG_INFO("Detected CIC chip 6105\n"); 112 | Memory::write(0xBFC007E6, 0x91); 113 | break; 114 | 115 | case 0xACC8580A: // 6106 116 | LOG_INFO("Detected CIC chip 6106\n"); 117 | Memory::write(0xBFC007E6, 0x85); 118 | break; 119 | 120 | default: 121 | LOG_WARN("Unknown IPL3 CRC32 value: 0x%08X\n", value); 122 | break; 123 | } 124 | 125 | if (FILE *pifFile = fopen("pif_rom.bin", "rb")) 126 | { 127 | // Load the PIF ROM into memory if it exists 128 | fread(memory, sizeof(uint8_t), 0x7C0, pifFile); 129 | fclose(pifFile); 130 | } 131 | else 132 | { 133 | // Set CPU registers as if the PIF ROM was executed 134 | // Values from https://github.com/mikeryan/n64dev/blob/master/src/boot/pif.S 135 | *CPU::registersW[1] = 0x0000000000000000; 136 | *CPU::registersW[2] = 0xFFFFFFFFD1731BE9; 137 | *CPU::registersW[3] = 0xFFFFFFFFD1731BE9; 138 | *CPU::registersW[4] = 0x0000000000001BE9; 139 | *CPU::registersW[5] = 0xFFFFFFFFF45231E5; 140 | *CPU::registersW[6] = 0xFFFFFFFFA4001F0C; 141 | *CPU::registersW[7] = 0xFFFFFFFFA4001F08; 142 | *CPU::registersW[8] = 0x00000000000000C0; 143 | *CPU::registersW[9] = 0x0000000000000000; 144 | *CPU::registersW[10] = 0x0000000000000040; 145 | *CPU::registersW[11] = 0xFFFFFFFFA4000040; 146 | *CPU::registersW[12] = 0xFFFFFFFFD1330BC3; 147 | *CPU::registersW[13] = 0xFFFFFFFFD1330BC3; 148 | *CPU::registersW[14] = 0x0000000025613A26; 149 | *CPU::registersW[15] = 0x000000002EA04317; 150 | *CPU::registersW[16] = 0x0000000000000000; 151 | *CPU::registersW[17] = 0x0000000000000000; 152 | *CPU::registersW[18] = 0x0000000000000000; 153 | *CPU::registersW[19] = 0x0000000000000000; 154 | *CPU::registersW[20] = 0x0000000000000001; 155 | *CPU::registersW[21] = 0x0000000000000000; 156 | *CPU::registersW[22] = Memory::read(0xBFC007E6); 157 | *CPU::registersW[23] = 0x0000000000000006; 158 | *CPU::registersW[24] = 0x0000000000000000; 159 | *CPU::registersW[25] = 0xFFFFFFFFD73F2993; 160 | *CPU::registersW[26] = 0x0000000000000000; 161 | *CPU::registersW[27] = 0x0000000000000000; 162 | *CPU::registersW[28] = 0x0000000000000000; 163 | *CPU::registersW[29] = 0xFFFFFFFFA4001FF0; 164 | *CPU::registersW[30] = 0x0000000000000000; 165 | *CPU::registersW[31] = 0xFFFFFFFFA4001554; 166 | 167 | // Copy the IPL3 from ROM to DMEM and jump to the start address 168 | for (uint32_t i = 0; i < 0x1000; i++) 169 | Memory::write(0xA4000000 + i, Core::rom[i]); 170 | CPU::programCounter = 0xA4000040 - 4; 171 | } 172 | 173 | // Set the memory size to 4MB 174 | // TODO: I think IPL3 is supposed to set this, but stubbing RI_SELECT_REG to 1 skips it 175 | Memory::write(0xA0000318, Settings::expansionPak ? 0x800000 : 0x400000); 176 | } 177 | 178 | void PIF::runCommand() 179 | { 180 | // Update the current command if new command bits were set 181 | if (uint8_t value = memory[0x7FF] & 0x7F) 182 | command = value; 183 | 184 | // Execute commands for any set bits, and clear the bits 185 | for (int i = 0; i < 7; i++) 186 | { 187 | if (command & (1 << i)) 188 | { 189 | (*pifCommands[i])(i); 190 | memory[0x7FF] &= ~(1 << i); 191 | } 192 | } 193 | } 194 | 195 | void PIF::pressKey(int key) 196 | { 197 | // Mark a button as pressed 198 | if (key < 16) 199 | buttons |= (1 << (15 - key)); 200 | } 201 | 202 | void PIF::releaseKey(int key) 203 | { 204 | // Mark a button as released 205 | if (key < 16) 206 | buttons &= ~(1 << (15 - key)); 207 | } 208 | 209 | void PIF::setStick(int x, int y) 210 | { 211 | // Update the stick position 212 | stickX = x; 213 | stickY = y; 214 | } 215 | 216 | void PIF::joybusProtocol(int bit) 217 | { 218 | uint8_t channel = 0; 219 | 220 | // Loop through PIF RAM and process joybus commands 221 | for (uint16_t i = 0x7C0; i < 0x7FF; i++) 222 | { 223 | int8_t txSize = memory[i]; 224 | 225 | if (txSize > 0) 226 | { 227 | uint8_t rxSize = memory[i + 1]; 228 | 229 | switch (uint8_t cmd = memory[i + 2]) 230 | { 231 | case 0xFF: // Reset 232 | case 0x00: // Info 233 | if (channel < 4) 234 | { 235 | // Report a standard controller with no pak 236 | memory[i + 3] = 0x05; // ID high 237 | memory[i + 4] = 0x00; // ID low 238 | memory[i + 5] = 0x02; // Status 239 | } 240 | else if (channel < 6) 241 | { 242 | // Report the current EEPROM type, if any 243 | memory[i + 3] = 0x00; // ID high 244 | memory[i + 4] = eepromId; // ID low 245 | memory[i + 5] = 0x00; // Status 246 | } 247 | else 248 | { 249 | // Report nothing 250 | memory[i + 3] = 0x00; // ID high 251 | memory[i + 4] = 0x00; // ID low 252 | memory[i + 5] = 0x00; // Status 253 | } 254 | break; 255 | 256 | case 0x01: // Controller state 257 | // Report the state of controller 1 if the channel is 0 258 | memory[i + 3] = channel ? 0 : (buttons >> 8); 259 | memory[i + 4] = channel ? 0 : (buttons >> 0); 260 | memory[i + 5] = channel ? 0 : stickX; 261 | memory[i + 6] = channel ? 0 : stickY; 262 | break; 263 | 264 | case 0x04: // Read EEPROM block 265 | if ((channel & ~1) == 4) // Channels 4 and 5 266 | { 267 | // Read 8 bytes from an EEPROM page to PIF memory 268 | uint16_t address = (memory[i + 3] * 8) & eepromMask; 269 | for (int j = 0; j < 8; j++) 270 | memory[i + 4 + j] = eepromMask ? Core::save[address + j] : 0; 271 | } 272 | break; 273 | 274 | case 0x05: // Write EEPROM block 275 | if ((channel & ~1) == 4 && eepromMask) // Channels 4 and 5 276 | { 277 | // Write 8 bytes from PIF memory to an EEPROM page 278 | uint16_t address = (memory[i + 3] * 8) & eepromMask; 279 | for (int j = 0; j < 8; j++) 280 | Core::writeSave(address + j, memory[i + 4 + j]); 281 | } 282 | break; 283 | 284 | default: 285 | LOG_WARN("Unknown joybus command: 0x%02X\n", cmd); 286 | break; 287 | } 288 | 289 | // Skip the transferred bytes and move to the next channel 290 | i += txSize + rxSize + 1; 291 | channel++; 292 | } 293 | else if (txSize == 0) 294 | { 295 | // Move to the next channel without transferring anything 296 | channel++; 297 | } 298 | else if (txSize == (int8_t)0xFE) 299 | { 300 | // Finish executing commands early 301 | return; 302 | } 303 | } 304 | } 305 | 306 | void PIF::verifyChecksum(int bit) 307 | { 308 | // On hardware, this command verifies if a checksum matches one given by the CIC 309 | // It doesn't matter for emulation, so just set the result bit 310 | memory[0x7FF] |= 0x80; 311 | } 312 | 313 | void PIF::clearMemory(int bit) 314 | { 315 | // Clear the 64 bytes of PIF RAM 316 | memset(&memory[0x7C0], 0, 0x40); 317 | } 318 | 319 | void PIF::unknownCmd(int bit) 320 | { 321 | // Warn about unknown commands 322 | LOG_WARN("Unknown PIF command bit: %d\n", bit); 323 | } 324 | -------------------------------------------------------------------------------- /src/cpu_cp0.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "cpu_cp0.h" 21 | #include "core.h" 22 | #include "cpu.h" 23 | #include "cpu_cp1.h" 24 | #include "log.h" 25 | #include "memory.h" 26 | #include "mi.h" 27 | 28 | namespace CPU_CP0 29 | { 30 | uint32_t _index; 31 | uint32_t entryLo0; 32 | uint32_t entryLo1; 33 | uint32_t context; 34 | uint32_t pageMask; 35 | uint32_t badVAddr; 36 | uint32_t count; 37 | uint32_t entryHi; 38 | uint32_t compare; 39 | uint32_t status; 40 | uint32_t cause; 41 | uint32_t epc; 42 | uint32_t errorEpc; 43 | 44 | bool irqPending; 45 | uint32_t startCycles; 46 | uint32_t endCycles; 47 | 48 | void scheduleCount(); 49 | void updateCount(); 50 | void interrupt(); 51 | 52 | void tlbr(uint32_t opcode); 53 | void tlbwi(uint32_t opcode); 54 | void tlbp(uint32_t opcode); 55 | void eret(uint32_t opcode); 56 | void unk(uint32_t opcode); 57 | } 58 | 59 | // CP0 instruction lookup table, using opcode bits 0-5 60 | void (*CPU_CP0::cp0Instrs[0x40])(uint32_t) = 61 | { 62 | unk, tlbr, tlbwi, unk, unk, unk, unk, unk, // 0x00-0x07 63 | tlbp, unk, unk, unk, unk, unk, unk, unk, // 0x08-0x0F 64 | unk, unk, unk, unk, unk, unk, unk, unk, // 0x10-0x17 65 | eret, unk, unk, unk, unk, unk, unk, unk, // 0x18-0x1F 66 | unk, unk, unk, unk, unk, unk, unk, unk, // 0x20-0x27 67 | unk, unk, unk, unk, unk, unk, unk, unk, // 0x28-0x2F 68 | unk, unk, unk, unk, unk, unk, unk, unk, // 0x30-0x37 69 | unk, unk, unk, unk, unk, unk, unk, unk // 0x38-0x3F 70 | }; 71 | 72 | void CPU_CP0::reset() 73 | { 74 | // Reset the CPU CP0 to its initial state 75 | _index = 0; 76 | entryLo0 = 0; 77 | entryLo1 = 0; 78 | context = 0; 79 | pageMask = 0; 80 | badVAddr = 0; 81 | count = 0; 82 | entryHi = 0; 83 | compare = 0; 84 | status = 0x400004; 85 | cause = 0; 86 | epc = 0; 87 | errorEpc = 0; 88 | irqPending = false; 89 | endCycles = -1; 90 | scheduleCount(); 91 | } 92 | 93 | int32_t CPU_CP0::read(int index) 94 | { 95 | // Read from a CPU CP0 register if one exists at the given index 96 | switch (index) 97 | { 98 | case 0: // Index 99 | // Get the index register 100 | return _index; 101 | 102 | case 2: // EntryLo0 103 | // Get the low entry 0 register 104 | return entryLo0; 105 | 106 | case 3: // EntryLo1 107 | // Get the low entry 1 register 108 | return entryLo1; 109 | 110 | case 4: // Context 111 | // Get the context register 112 | return context; 113 | 114 | case 5: // PageMask 115 | // Get the page mask register 116 | return pageMask; 117 | 118 | case 8: // BadVAddr 119 | // Get the bad virtual address register 120 | return badVAddr; 121 | 122 | case 9: // Count 123 | // Get the count register, as it would be at the current cycle 124 | return count + ((Core::globalCycles - startCycles) >> 2); 125 | 126 | case 10: // EntryHi 127 | // Get the high entry register 128 | return entryHi; 129 | 130 | case 11: // Compare 131 | // Get the compare register 132 | return compare; 133 | 134 | case 12: // Status 135 | // Get the status register 136 | return status; 137 | 138 | case 13: // Cause 139 | // Get the cause register 140 | return cause; 141 | 142 | case 14: // EPC 143 | // Get the exception program counter 144 | return epc; 145 | 146 | case 30: // ErrorEPC 147 | // Get the error exception program counter 148 | return errorEpc; 149 | 150 | default: 151 | LOG_WARN("Read from unknown CPU CP0 register: %d\n", index); 152 | return 0; 153 | } 154 | } 155 | 156 | void CPU_CP0::write(int index, int32_t value) 157 | { 158 | // Write to a CPU CP0 register if one exists at the given index 159 | switch (index) 160 | { 161 | case 0: // Index 162 | // Set the index register 163 | _index = value & 0x3F; 164 | return; 165 | 166 | case 2: // EntryLo0 167 | // Set the low entry 0 register 168 | entryLo0 = value & 0x3FFFFFF; 169 | return; 170 | 171 | case 3: // EntryLo1 172 | // Set the low entry 1 register 173 | entryLo1 = value & 0x3FFFFFF; 174 | return; 175 | 176 | case 4: // Context 177 | // Set the context register 178 | context = value & 0xFFFFFFF0; 179 | return; 180 | 181 | case 5: // PageMask 182 | // Set the page mask register 183 | pageMask = value & 0x1FFE000; 184 | return; 185 | 186 | case 9: // Count 187 | // Set the count register and reschedule its next update 188 | count = value; 189 | scheduleCount(); 190 | return; 191 | 192 | case 10: // EntryHi 193 | // Set the high entry register 194 | entryHi = value & 0xFFFFE0FF; 195 | return; 196 | 197 | case 11: // Compare 198 | // Set the compare register and acknowledge a timer interrupt 199 | compare = value; 200 | cause &= ~0x8000; 201 | 202 | // Update the count register and reschedule its next update 203 | count += ((Core::globalCycles - startCycles) >> 2); 204 | scheduleCount(); 205 | return; 206 | 207 | case 12: // Status 208 | // Set the status register and apply the FR bit to the CP1 209 | status = value & 0xFF57FFFF; 210 | checkInterrupts(); 211 | CPU_CP1::setRegMode(status & (1 << 26)); 212 | 213 | // Keep track of unimplemented bits that should do something 214 | if (uint32_t bits = (value & 0xB0000E0)) 215 | LOG_WARN("Unimplemented CPU CP0 status bits set: 0x%X\n", bits); 216 | return; 217 | 218 | case 13: // Cause 219 | // Set the software interrupt flags 220 | cause = (cause & ~0x300) | (value & 0x300); 221 | checkInterrupts(); 222 | return; 223 | 224 | case 14: // EPC 225 | // Set the exception program counter 226 | epc = value; 227 | return; 228 | 229 | case 30: // ErrorEPC 230 | // Set the error exception program counter 231 | errorEpc = value; 232 | return; 233 | 234 | default: 235 | LOG_WARN("Write to unknown CPU CP0 register: %d\n", index); 236 | return; 237 | } 238 | } 239 | 240 | void CPU_CP0::resetCycles() 241 | { 242 | // Adjust the cycle counts for a cycle reset 243 | startCycles -= Core::globalCycles; 244 | endCycles -= Core::globalCycles; 245 | } 246 | 247 | void CPU_CP0::scheduleCount() 248 | { 249 | // Assuming count is updated, schedule its next update 250 | // This is done as close to match as possible, with a limit to prevent cycle overflow 251 | startCycles = Core::globalCycles; 252 | uint32_t cycles = startCycles + std::min((compare - count) << 2, 0x40000000); 253 | cycles += (startCycles == cycles) << 2; 254 | 255 | // Only reschedule if the update is sooner than what's already scheduled 256 | // This helps prevent overloading the scheduler when registers are used excessively 257 | if (endCycles > cycles) 258 | { 259 | Core::schedule(updateCount, cycles - startCycles); 260 | endCycles = cycles; 261 | } 262 | } 263 | 264 | void CPU_CP0::updateCount() 265 | { 266 | // Ignore the update if it was rescheduled 267 | if (Core::globalCycles != endCycles) 268 | return; 269 | 270 | // Update count and request a timer interrupt if it matches compare 271 | if ((count += ((endCycles - startCycles) >> 2)) == compare) 272 | { 273 | cause |= 0x8000; 274 | checkInterrupts(); 275 | } 276 | 277 | // Schedule the next update unconditionally 278 | endCycles = -1; 279 | scheduleCount(); 280 | } 281 | 282 | void CPU_CP0::checkInterrupts() 283 | { 284 | // Set the external interrupt bit if any MI interrupt is set 285 | cause = (cause & ~0x400) | ((bool)(MI::interrupt & MI::mask) << 10); 286 | 287 | // Schedule an interrupt if able and an enabled bit is set 288 | if (((status & 0x3) == 0x1) && (status & cause & 0xFF00) && !irqPending) 289 | { 290 | Core::schedule(interrupt, 2); // 1 CPU cycle 291 | irqPending = true; 292 | } 293 | } 294 | 295 | void CPU_CP0::interrupt() 296 | { 297 | // Trigger an interrupt that has been scheduled 298 | CPU_CP0::exception(0); 299 | irqPending = false; 300 | } 301 | 302 | void CPU_CP0::exception(uint8_t type) 303 | { 304 | // Update registers for an exception and jump to the handler 305 | // TODO: handle nested exceptions 306 | status |= 0x2; // EXL 307 | cause = (cause & ~0x8000007C) | ((type << 2) & 0x7C); 308 | epc = CPU::programCounter - (type ? 4 : 0); 309 | CPU::programCounter = ((status & (1 << 22)) ? 0xBFC00200 : 0x80000000) - 4; 310 | CPU::nextOpcode = 0; 311 | 312 | // Adjust the exception vector based on the type 313 | if ((type & ~1) != 2) // Not TLB miss 314 | CPU::programCounter += 0x180; 315 | 316 | // Return to the preceding branch if the exception occured in a delay slot 317 | if (CPU::delaySlot != -1) 318 | { 319 | epc = CPU::delaySlot - 4; 320 | cause |= (1 << 31); // BD 321 | } 322 | 323 | // Unhalt the CPU if it was idling 324 | Core::cpuRunning = true; 325 | } 326 | 327 | void CPU_CP0::setTlbAddress(uint32_t address) 328 | { 329 | // Set the address that caused a TLB exception 330 | badVAddr = address; 331 | entryHi = address & 0xFFFFE000; 332 | context = (context & ~0x7FFFF0) | ((address >> 9) & 0x7FFFF0); 333 | } 334 | 335 | bool CPU_CP0::cpUsable(uint8_t cp) 336 | { 337 | // Check if a coprocessor is usable (CP0 is always usable in kernel mode) 338 | if (!(status & (1 << (28 + cp))) && (cp > 0 || (!(status & 0x6) && (status & 0x18)))) 339 | { 340 | // Set the coprocessor number bits 341 | cause = (cause & ~(0x3 << 28)) | ((cp & 0x3) << 28); 342 | return false; 343 | } 344 | 345 | return true; 346 | } 347 | 348 | void CPU_CP0::tlbr(uint32_t opcode) 349 | { 350 | // Get the TLB entry at the current index 351 | Memory::getEntry(_index, entryLo0, entryLo1, entryHi, pageMask); 352 | } 353 | 354 | void CPU_CP0::tlbwi(uint32_t opcode) 355 | { 356 | // Set the TLB entry at the current index 357 | Memory::setEntry(_index, entryLo0, entryLo1, entryHi, pageMask); 358 | } 359 | 360 | void CPU_CP0::tlbp(uint32_t opcode) 361 | { 362 | // Search the TLB entries for one that matches the current high register 363 | for (int i = 0; i < 32; i++) 364 | { 365 | // Get a TLB entry 366 | uint32_t _entryLo0, _entryLo1, _entryHi, _pageMask; 367 | Memory::getEntry(i, _entryLo0, _entryLo1, _entryHi, _pageMask); 368 | 369 | // Set the index to the TLB entry if it matches 370 | if (entryHi == _entryHi) 371 | { 372 | _index = i; 373 | return; 374 | } 375 | } 376 | 377 | // Set the index high bit if no match was found 378 | _index = (1 << 31); 379 | } 380 | 381 | void CPU_CP0::eret(uint32_t opcode) 382 | { 383 | // Return from an error exception or exception and clear the ERL or EXL bit 384 | CPU::programCounter = CPU_CP0::read((status & 0x4) ? 30 : 14) - 4; 385 | CPU::nextOpcode = 0; 386 | status &= ~((status & 0x4) ? 0x4 : 0x2); 387 | } 388 | 389 | void CPU_CP0::unk(uint32_t opcode) 390 | { 391 | // Warn about unknown instructions 392 | LOG_CRIT("Unknown CP0 opcode: 0x%08X @ 0x%X\n", opcode, CPU::programCounter - 4); 393 | } 394 | -------------------------------------------------------------------------------- /src/switch/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "switch_ui.h" 28 | #include "../ai.h" 29 | #include "../core.h" 30 | #include "../pif.h" 31 | #include "../settings.h" 32 | #include "../vi.h" 33 | 34 | AudioOutBuffer audioBuffers[2]; 35 | AudioOutBuffer *audioReleasedBuffer; 36 | int16_t *audioData[2]; 37 | uint32_t count; 38 | 39 | std::string path; 40 | std::thread *audioThread; 41 | bool showFps; 42 | 43 | const uint32_t keyMap[] = 44 | { 45 | (HidNpadButton_A | HidNpadButton_B), (HidNpadButton_X | HidNpadButton_Y), // A, B 46 | (HidNpadButton_ZL | HidNpadButton_ZR), HidNpadButton_Plus, // Z, Start 47 | HidNpadButton_Up, HidNpadButton_Down, HidNpadButton_Left, HidNpadButton_Right, // D-pad 48 | 0, 0, HidNpadButton_L, HidNpadButton_R, // L, R 49 | HidNpadButton_StickRUp, HidNpadButton_StickRDown, // C-up, C-down 50 | HidNpadButton_StickRLeft, HidNpadButton_StickRRight, // C-left, C-right 51 | (HidNpadButton_StickL | HidNpadButton_StickR), HidNpadButton_Minus // FPS, Pause 52 | }; 53 | 54 | void outputAudio() 55 | { 56 | while (Core::running) 57 | { 58 | // Load audio samples from the core when a buffer is empty 59 | audoutWaitPlayFinish(&audioReleasedBuffer, &count, UINT64_MAX); 60 | AI::fillBuffer((uint32_t*)audioReleasedBuffer->buffer); 61 | audoutAppendAudioOutBuffer(audioReleasedBuffer); 62 | } 63 | } 64 | 65 | bool startCore(bool reset) 66 | { 67 | if (!audioThread) 68 | { 69 | // Try to boot a ROM at the current path, but display an error if failed 70 | if (reset && !Core::bootRom(path)) 71 | { 72 | std::vector message = { "Make sure the ROM file is accessible and try again." }; 73 | SwitchUI::message("Error Loading ROM", message); 74 | return false; 75 | } 76 | 77 | // Start the emulator core 78 | Core::start(); 79 | audioThread = new std::thread(outputAudio); 80 | } 81 | 82 | return true; 83 | } 84 | 85 | void stopCore() 86 | { 87 | if (audioThread) 88 | { 89 | // Stop the emulator core 90 | Core::stop(); 91 | audioThread->join(); 92 | delete audioThread; 93 | audioThread = nullptr; 94 | } 95 | } 96 | 97 | void settingsMenu() 98 | { 99 | const std::vector toggle = { "Off", "On" }; 100 | size_t index = 0; 101 | 102 | while (true) 103 | { 104 | // Make a list of settings and current values 105 | std::vector settings = 106 | { 107 | ListItem("FPS Limiter", toggle[Settings::fpsLimiter]), 108 | ListItem("Expansion Pak", toggle[Settings::expansionPak]), 109 | ListItem("Threaded RDP", toggle[Settings::threadedRdp]), 110 | ListItem("Texture Filter", toggle[Settings::texFilter]) 111 | }; 112 | 113 | // Create the settings menu 114 | Selection menu = SwitchUI::menu("Settings", &settings, index); 115 | index = menu.index; 116 | 117 | // Handle menu input 118 | if (menu.pressed & HidNpadButton_A) 119 | { 120 | // Change the chosen setting to its next value 121 | switch (index) 122 | { 123 | case 0: Settings::fpsLimiter = !Settings::fpsLimiter; break; 124 | case 1: Settings::expansionPak = !Settings::expansionPak; break; 125 | case 2: Settings::threadedRdp = !Settings::threadedRdp; break; 126 | case 3: Settings::texFilter = !Settings::texFilter; break; 127 | } 128 | } 129 | else 130 | { 131 | // Close the settings menu 132 | Settings::save(); 133 | return; 134 | } 135 | } 136 | } 137 | 138 | void fileBrowser() 139 | { 140 | size_t index = 0; 141 | path = "sdmc:/"; 142 | 143 | // Load the appropriate icons for the current theme 144 | uint32_t *file = SwitchUI::bmpToTexture(SwitchUI::isDarkTheme() ? "romfs:/file-dark.bmp" : "romfs:/file-light.bmp"); 145 | uint32_t *folder = SwitchUI::bmpToTexture(SwitchUI::isDarkTheme() ? "romfs:/folder-dark.bmp" : "romfs:/folder-light.bmp"); 146 | 147 | while (true) 148 | { 149 | std::vector files; 150 | DIR *dir = opendir(path.c_str()); 151 | dirent *entry; 152 | 153 | // Add all folders and ROMs at the current path to a list with icons 154 | while ((entry = readdir(dir))) 155 | { 156 | std::string name = entry->d_name; 157 | if (entry->d_type == DT_DIR) 158 | files.push_back(ListItem(name, "", folder, 64)); 159 | else if (name.find(".z64", name.length() - 4) != std::string::npos) 160 | files.push_back(ListItem(name, "", file, 64)); 161 | } 162 | 163 | closedir(dir); 164 | sort(files.begin(), files.end()); 165 | 166 | // Create the file browser menu 167 | Selection menu = SwitchUI::menu("rokuyon", &files, index, "Settings", "Exit"); 168 | index = menu.index; 169 | 170 | // Handle menu input 171 | if (menu.pressed & HidNpadButton_A) 172 | { 173 | if (!files.empty()) 174 | { 175 | // Navigate to the selected path 176 | path += "/" + files[menu.index].name; 177 | index = 0; 178 | 179 | if (files[menu.index].icon == file) 180 | { 181 | // Close the browser If a ROM is loaded successfully 182 | if (startCore(true)) 183 | break; 184 | 185 | // Remove the ROM from the path and continue browsing 186 | path = path.substr(0, path.rfind("/")); 187 | } 188 | } 189 | } 190 | else if (menu.pressed & HidNpadButton_B) 191 | { 192 | if (path != "sdmc:/") 193 | { 194 | // Navigate to the previous directory 195 | path = path.substr(0, path.rfind("/")); 196 | index = 0; 197 | } 198 | } 199 | else if (menu.pressed & HidNpadButton_X) 200 | { 201 | // Open the settings menu 202 | settingsMenu(); 203 | } 204 | else 205 | { 206 | // Close the file browser 207 | break; 208 | } 209 | } 210 | 211 | // Free the theme icons 212 | delete[] file; 213 | delete[] folder; 214 | } 215 | 216 | bool saveTypeMenu() 217 | { 218 | size_t index = 0; 219 | std::vector items = 220 | { 221 | ListItem("None"), 222 | ListItem("EEPROM 0.5KB"), 223 | ListItem("EEPROM 2KB"), 224 | ListItem("SRAM 32KB"), 225 | ListItem("FLASH 128KB") 226 | }; 227 | 228 | // Select the current save type by default 229 | switch (Core::saveSize) 230 | { 231 | case 0x00200: index = 1; break; // EEPROM 0.5KB 232 | case 0x00800: index = 2; break; // EEPROM 8KB 233 | case 0x08000: index = 3; break; // SRAM 32KB 234 | case 0x20000: index = 4; break; // FLASH 128KB 235 | } 236 | 237 | // Create the save type menu 238 | Selection menu = SwitchUI::menu("Change Save Type", &items, index); 239 | index = menu.index; 240 | 241 | // Handle menu input 242 | if (menu.pressed & HidNpadButton_A) 243 | { 244 | // Ask for confirmation before doing anything because accidents could be bad! 245 | std::vector message = { "Are you sure? This may result in data loss!" }; 246 | if (!SwitchUI::message("Changing Save Type", message, true)) 247 | return false; 248 | 249 | // On confirmation, change the save type 250 | switch (index) 251 | { 252 | case 0: Core::resizeSave(0x00000); break; // None 253 | case 1: Core::resizeSave(0x00200); break; // EEPROM 0.5KB 254 | case 2: Core::resizeSave(0x00800); break; // EEPROM 8KB 255 | case 3: Core::resizeSave(0x08000); break; // SRAM 32KB 256 | case 4: Core::resizeSave(0x20000); break; // FLASH 128KB 257 | } 258 | 259 | // Restart the emulator 260 | Core::bootRom(path); 261 | return true; 262 | } 263 | 264 | return false; 265 | } 266 | 267 | void pauseMenu() 268 | { 269 | size_t index = 0; 270 | std::vector items = 271 | { 272 | ListItem("Resume"), 273 | ListItem("Restart"), 274 | ListItem("Change Save Type"), 275 | ListItem("Settings"), 276 | ListItem("File Browser") 277 | }; 278 | 279 | // Pause the emulator 280 | stopCore(); 281 | 282 | while (true) 283 | { 284 | // Create the pause menu 285 | Selection menu = SwitchUI::menu("rokuyon", &items, index); 286 | index = menu.index; 287 | 288 | // Handle menu input 289 | if (menu.pressed & HidNpadButton_A) 290 | { 291 | switch (index) 292 | { 293 | case 0: // Resume 294 | // Return to the emulator 295 | startCore(false); 296 | return; 297 | 298 | case 2: // Change Save Type 299 | // Open the save type menu and restart if the save changed 300 | if (!saveTypeMenu()) 301 | break; 302 | 303 | case 1: // Restart 304 | // Restart and return to the emulator 305 | if (!startCore(true)) 306 | fileBrowser(); 307 | return; 308 | 309 | case 3: // Settings 310 | // Open the settings menu 311 | settingsMenu(); 312 | break; 313 | 314 | case 4: // File Browser 315 | // Open the file browser 316 | fileBrowser(); 317 | return; 318 | } 319 | } 320 | else if (menu.pressed & HidNpadButton_B) 321 | { 322 | // Return to the emulator 323 | startCore(false); 324 | return; 325 | } 326 | else 327 | { 328 | // Close the pause menu 329 | return; 330 | } 331 | } 332 | } 333 | 334 | int main() 335 | { 336 | // Initialize the UI and lock exiting until cleanup 337 | appletLockExit(); 338 | SwitchUI::initialize(); 339 | 340 | // Load settings or create them if they don't exist 341 | if (!Settings::load()) 342 | Settings::save(); 343 | 344 | // Initialize audio output 345 | audoutInitialize(); 346 | audoutStartAudioOut(); 347 | 348 | // Initialize the audio buffers 349 | for (int i = 0; i < 2; i++) 350 | { 351 | size_t size = 1024 * 2 * sizeof(int16_t); 352 | audioData[i] = (int16_t*)memalign(0x1000, size); 353 | memset(audioData[i], 0, size); 354 | audioBuffers[i].next = nullptr; 355 | audioBuffers[i].buffer = audioData[i]; 356 | audioBuffers[i].buffer_size = size; 357 | audioBuffers[i].data_size = size; 358 | audioBuffers[i].data_offset = 0; 359 | audoutAppendAudioOutBuffer(&audioBuffers[i]); 360 | } 361 | 362 | // Overclock the Switch CPU 363 | clkrstInitialize(); 364 | ClkrstSession cpuSession; 365 | clkrstOpenSession(&cpuSession, PcvModuleId_CpuBus, 0); 366 | clkrstSetClockRate(&cpuSession, 1785000000); 367 | 368 | // Open the file browser 369 | fileBrowser(); 370 | 371 | while (appletMainLoop() && Core::running) 372 | { 373 | // Maintain the CPU overclock if it was reset from ex. leaving the app 374 | uint32_t rate; 375 | clkrstGetClockRate(&cpuSession, &rate); 376 | if (rate != 1785000000) 377 | clkrstSetClockRate(&cpuSession, 1785000000); 378 | 379 | // Scan for controller input 380 | padUpdate(SwitchUI::getPad()); 381 | uint32_t pressed = padGetButtonsDown(SwitchUI::getPad()); 382 | uint32_t released = padGetButtonsUp(SwitchUI::getPad()); 383 | HidAnalogStickState stick = padGetStickPos(SwitchUI::getPad(), 0); 384 | 385 | // Send key input to the core 386 | for (int i = 0; i < 16; i++) 387 | { 388 | if (pressed & keyMap[i]) 389 | PIF::pressKey(i); 390 | else if (released & keyMap[i]) 391 | PIF::releaseKey(i); 392 | } 393 | 394 | // Send joystick input to the core 395 | PIF::setStick(stick.x >> 8, stick.y >> 8); 396 | 397 | // Draw a new frame if one is ready 398 | if (_Framebuffer *fb = VI::getFramebuffer()) 399 | { 400 | SwitchUI::clear(Color(0, 0, 0)); 401 | SwitchUI::drawImage(fb->data, fb->width, fb->height, 160, 0, 960, 720, true, 0); 402 | if (showFps) SwitchUI::drawString(std::to_string(Core::fps) + " FPS", 5, 0, 48, Color(255, 255, 255)); 403 | SwitchUI::update(); 404 | delete fb; 405 | } 406 | 407 | // Toggle showing FPS or open the pause menu if hotkeys are pressed 408 | if (pressed & keyMap[16]) 409 | showFps = !showFps; 410 | else if (pressed & keyMap[17]) 411 | pauseMenu(); 412 | } 413 | 414 | // Ensure the core is stopped 415 | stopCore(); 416 | 417 | // Disable the CPU overclock 418 | clkrstSetClockRate(&cpuSession, 1020000000); 419 | clkrstExit(); 420 | 421 | // Stop audio output 422 | audoutStopAudioOut(); 423 | audoutExit(); 424 | 425 | // Free the audio buffers 426 | delete[] audioData[0]; 427 | delete[] audioData[1]; 428 | 429 | // Clean up the UI and unlock exiting 430 | SwitchUI::deinitialize(); 431 | appletUnlockExit(); 432 | return 0; 433 | } 434 | -------------------------------------------------------------------------------- /src/memory.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | 23 | #include "memory.h" 24 | #include "ai.h" 25 | #include "core.h" 26 | #include "cpu_cp0.h" 27 | #include "log.h" 28 | #include "mi.h" 29 | #include "pi.h" 30 | #include "pif.h" 31 | #include "rdp.h" 32 | #include "rsp.h" 33 | #include "rsp_cp0.h" 34 | #include "settings.h" 35 | #include "si.h" 36 | #include "vi.h" 37 | 38 | enum FlashState 39 | { 40 | FLASH_NONE = 0, 41 | FLASH_STATUS, 42 | FLASH_READ, 43 | FLASH_WRITE, 44 | FLASH_ERASE 45 | }; 46 | 47 | struct TLBEntry 48 | { 49 | uint32_t entryLo0; 50 | uint32_t entryLo1; 51 | uint32_t entryHi; 52 | uint32_t pageMask; 53 | }; 54 | 55 | namespace Memory 56 | { 57 | uint8_t rdram[0x800000]; // 8MB RDRAM 58 | uint8_t rspMem[0x2000]; // 4KB RSP DMEM + 4KB RSP IMEM 59 | TLBEntry entries[32]; 60 | uint32_t ramSize; 61 | 62 | uint8_t writeBuf[0x80]; 63 | uint64_t status; 64 | uint32_t writeOfs; 65 | uint32_t eraseOfs; 66 | FlashState state; 67 | 68 | void writeFlash(uint32_t value); 69 | } 70 | 71 | void Memory::reset() 72 | { 73 | // Reset memory to its initial state 74 | memset(rdram, 0, sizeof(rdram)); 75 | memset(rspMem, 0, sizeof(rspMem)); 76 | memset(writeBuf, 0, sizeof(writeBuf)); 77 | ramSize = Settings::expansionPak ? 0x800000 : 0x400000; 78 | writeOfs = 0; 79 | eraseOfs = 0; 80 | state = FLASH_NONE; 81 | 82 | // Map TLB entries to inaccessible locations 83 | for (int i = 0; i < 32; i++) 84 | entries[i].entryHi = 0x80000000; 85 | } 86 | 87 | void Memory::getEntry(uint32_t index, uint32_t &entryLo0, uint32_t &entryLo1, uint32_t &entryHi, uint32_t &pageMask) 88 | { 89 | // Get the TLB entry at the given index 90 | TLBEntry &entry = entries[index & 0x1F]; 91 | entryLo0 = entry.entryLo0; 92 | entryLo1 = entry.entryLo1; 93 | entryHi = entry.entryHi; 94 | pageMask = entry.pageMask; 95 | } 96 | 97 | void Memory::setEntry(uint32_t index, uint32_t entryLo0, uint32_t entryLo1, uint32_t entryHi, uint32_t pageMask) 98 | { 99 | // Set the TLB entry at the given index 100 | TLBEntry &entry = entries[index & 0x1F]; 101 | entry.entryLo0 = entryLo0; 102 | entry.entryLo1 = entryLo1; 103 | entry.entryHi = entryHi; 104 | entry.pageMask = pageMask; 105 | } 106 | 107 | template uint8_t Memory::read(uint32_t address); 108 | template uint16_t Memory::read(uint32_t address); 109 | template uint32_t Memory::read(uint32_t address); 110 | template uint64_t Memory::read(uint32_t address); 111 | template T Memory::read(uint32_t address) 112 | { 113 | uint8_t *data = nullptr; 114 | uint32_t pAddr = 0x80000000; 115 | 116 | // Get a physical address from a virtual one 117 | if ((address & 0xC0000000) == 0x80000000) // kseg0, kseg1 118 | { 119 | // Mask the virtual address to get a physical one 120 | pAddr = address & 0x1FFFFFFF; 121 | } 122 | else // TLB 123 | { 124 | // Search the TLB entries for a page that contains the virtual address 125 | // TODO: actually use the ASID and CDVG bits, and support TLB invalid exceptions 126 | for (int i = 0; i < 32; i++) 127 | { 128 | uint32_t vAddr = entries[i].entryHi & 0xFFFFE000; 129 | uint32_t mask = entries[i].pageMask | 0x1FFF; 130 | 131 | if (address - vAddr <= mask) 132 | { 133 | // Choose between the even or odd physical pages, and add the masked offset 134 | if (address - vAddr <= (mask >> 1)) 135 | pAddr = ((entries[i].entryLo0 & 0x3FFFFC0) << 6) + (address & (mask >> 1)); 136 | else 137 | pAddr = ((entries[i].entryLo1 & 0x3FFFFC0) << 6) + (address & (mask >> 1)); 138 | goto lookup; 139 | } 140 | } 141 | 142 | // Trigger a TLB load miss exception if a TLB entry wasn't found 143 | CPU_CP0::exception(2); 144 | CPU_CP0::setTlbAddress(address); 145 | return 0; 146 | } 147 | 148 | lookup: 149 | // Look up the physical address 150 | if (pAddr < ramSize) 151 | { 152 | // Get a pointer to data in RDRAM 153 | // TODO: figure out RDRAM registers and how they affect mapping 154 | data = &rdram[pAddr]; 155 | } 156 | else if (pAddr >= 0x4000000 && pAddr < 0x4040000) 157 | { 158 | // Read a value from RSP DMEM/IMEM, with wraparound 159 | T value = 0; 160 | for (size_t i = 0; i < sizeof(T); i++) 161 | value |= (T)rspMem[(pAddr & 0x1000) | ((pAddr + i) & 0xFFF)] << ((sizeof(T) - 1 - i) * 8); 162 | return value; 163 | } 164 | else if (pAddr >= 0x8000000 && pAddr < 0x8008000 && Core::saveSize == 0x8000) 165 | { 166 | // Get a pointer to data in cart SRAM, if it exists 167 | data = &Core::save[pAddr & 0x7FFF]; 168 | } 169 | else if (pAddr >= 0x8000000 && pAddr < 0x8020000 && Core::saveSize == 0x20000) 170 | { 171 | // Get a pointer to data in cart FLASH, if it's readable 172 | if (state == FLASH_READ) 173 | data = &Core::save[address & 0x1FFFF]; 174 | else 175 | return status >> ((~(address + sizeof(T) - 1) & 0x7) * 8); 176 | } 177 | else if (pAddr >= 0x10000000 && pAddr < 0x10000000 + std::min(Core::romSize, 0xFC00000U)) 178 | { 179 | // Get a pointer to data in cart ROM 180 | data = &Core::rom[pAddr - 0x10000000]; 181 | } 182 | else if (pAddr >= 0x1FC00000 && pAddr < 0x1FC00800) 183 | { 184 | // Get a pointer to data in PIF ROM/RAM 185 | data = &PIF::memory[pAddr & 0x7FF]; 186 | } 187 | else if (sizeof(T) != sizeof(uint32_t)) 188 | { 189 | // Ignore I/O writes that aren't 32-bit 190 | } 191 | else if (pAddr >= 0x4040000 && pAddr < 0x4040020) 192 | { 193 | // Read a value from an RSP CP0 register 194 | return RSP_CP0::read((pAddr & 0x1F) >> 2); 195 | } 196 | else if (pAddr == 0x4080000) 197 | { 198 | // Read a value from the RSP program counter 199 | return RSP::readPC(); 200 | } 201 | else if (pAddr >= 0x4100000 && pAddr < 0x4100020) 202 | { 203 | // Read a value from an RDP register 204 | return RDP::read((pAddr & 0x1F) >> 2); 205 | } 206 | else if (pAddr == 0x470000C) 207 | { 208 | // Stub the RI_SELECT register 209 | return 0x1; 210 | } 211 | else 212 | { 213 | // Read a value from a group of registers 214 | switch (pAddr >> 20) 215 | { 216 | case 0x43: return MI::read(pAddr); 217 | case 0x44: return VI::read(pAddr); 218 | case 0x45: return AI::read(pAddr); 219 | case 0x46: return PI::read(pAddr); 220 | case 0x48: return SI::read(pAddr); 221 | } 222 | } 223 | 224 | if (data != nullptr) 225 | { 226 | // Read a value from the pointer, big-endian style (MSB first) 227 | T value = 0; 228 | for (size_t i = 0; i < sizeof(T); i++) 229 | value |= (T)data[i] << ((sizeof(T) - 1 - i) * 8); 230 | return value; 231 | } 232 | 233 | LOG_WARN("Unknown memory read: 0x%X\n", address); 234 | return 0; 235 | } 236 | 237 | template void Memory::write(uint32_t address, uint8_t value); 238 | template void Memory::write(uint32_t address, uint16_t value); 239 | template void Memory::write(uint32_t address, uint32_t value); 240 | template void Memory::write(uint32_t address, uint64_t value); 241 | template void Memory::write(uint32_t address, T value) 242 | { 243 | uint8_t *data = nullptr; 244 | uint32_t pAddr = 0x80000000; 245 | 246 | // Get a physical address from a virtual one 247 | if ((address & 0xC0000000) == 0x80000000) // kseg0, kseg1 248 | { 249 | // Mask the virtual address to get a physical one 250 | pAddr = address & 0x1FFFFFFF; 251 | } 252 | else // TLB 253 | { 254 | // Search the TLB entries for a page that contains the virtual address 255 | // TODO: actually use the ASID and CDVG bits, and support TLB invalid exceptions 256 | for (int i = 0; i < 32; i++) 257 | { 258 | uint32_t vAddr = entries[i].entryHi & 0xFFFFE000; 259 | uint32_t mask = entries[i].pageMask | 0x1FFF; 260 | 261 | if (address - vAddr <= mask) 262 | { 263 | // Choose between the even or odd physical pages, and add the masked offset 264 | if (address - vAddr <= (mask >> 1)) 265 | { 266 | if (entries[i].entryLo0 & 0x4) // Dirty 267 | { 268 | pAddr = ((entries[i].entryLo0 & 0x3FFFFC0) << 6) + (address & (mask >> 1)); 269 | goto lookup; 270 | } 271 | } 272 | else 273 | { 274 | if (entries[i].entryLo1 & 0x4) // Dirty 275 | { 276 | pAddr = ((entries[i].entryLo1 & 0x3FFFFC0) << 6) + (address & (mask >> 1)); 277 | goto lookup; 278 | } 279 | } 280 | 281 | // Trigger a TLB modification exception if the TLB entry isn't writable 282 | CPU_CP0::exception(1); 283 | CPU_CP0::setTlbAddress(address); 284 | return; 285 | } 286 | } 287 | 288 | // Trigger a TLB store miss exception if a TLB entry wasn't found 289 | CPU_CP0::exception(3); 290 | CPU_CP0::setTlbAddress(address); 291 | return; 292 | } 293 | 294 | lookup: 295 | // Look up the physical address 296 | if (pAddr < ramSize) 297 | { 298 | // Get a pointer to data in RDRAM 299 | // TODO: figure out RDRAM registers and how they affect mapping 300 | data = &rdram[pAddr]; 301 | } 302 | else if (pAddr >= 0x4000000 && pAddr < 0x4040000) 303 | { 304 | // Write a value to RSP DMEM/IMEM, with wraparound 305 | for (size_t i = 0; i < sizeof(T); i++) 306 | rspMem[(pAddr & 0x1000) | ((pAddr + i) & 0xFFF)] = value >> ((sizeof(T) - 1 - i) * 8); 307 | return; 308 | } 309 | else if (pAddr >= 0x8000000 && pAddr < 0x8008000 && Core::saveSize == 0x8000) 310 | { 311 | // Write a value to cart SRAM, if it exists 312 | for (size_t i = 0; i < sizeof(T); i++) 313 | Core::writeSave((pAddr + i) & 0x7FFF, value >> ((sizeof(T) - 1 - i) * 8)); 314 | return; 315 | } 316 | else if (pAddr >= 0x8000000 && pAddr < 0x8000080 && state == FLASH_WRITE) 317 | { 318 | // Get a pointer to data in the FLASH write buffer, if it's writable 319 | data = &writeBuf[address & 0x7F]; 320 | } 321 | else if (pAddr >= 0x1FC007C0 && pAddr < 0x1FC00800) 322 | { 323 | // Get a pointer to data in PIF ROM/RAM 324 | data = &PIF::memory[pAddr & 0x7FF]; 325 | 326 | // Catch writes to the PIF command byte and call the PIF 327 | if (pAddr >= 0x1FC00800 - sizeof(T)) 328 | { 329 | for (size_t i = 0; i < sizeof(T); i++) 330 | data[i] = value >> ((sizeof(T) - 1 - i) * 8); 331 | PIF::runCommand(); 332 | return; 333 | } 334 | } 335 | else if (sizeof(T) != sizeof(uint32_t)) 336 | { 337 | // Ignore I/O writes that aren't 32-bit 338 | } 339 | else if (pAddr >= 0x4040000 && pAddr < 0x4040020) 340 | { 341 | // Write a value to an RSP CP0 register 342 | return RSP_CP0::write((pAddr & 0x1F) >> 2, value); 343 | } 344 | else if (pAddr == 0x4080000) 345 | { 346 | // Write a value to the RSP program counter 347 | return RSP::writePC(value); 348 | } 349 | else if (pAddr >= 0x4100000 && pAddr < 0x4100020) 350 | { 351 | // Write a value to an RDP register 352 | return RDP::write((pAddr & 0x1F) >> 2, value); 353 | } 354 | else if (pAddr == 0x8010000 && Core::saveSize == 0x20000) 355 | { 356 | // Write a value to the FLASH register 357 | return writeFlash(value); 358 | } 359 | else 360 | { 361 | // Write a value to a group of registers 362 | switch (pAddr >> 20) 363 | { 364 | case 0x43: return MI::write(pAddr, value); 365 | case 0x44: return VI::write(pAddr, value); 366 | case 0x45: return AI::write(pAddr, value); 367 | case 0x46: return PI::write(pAddr, value); 368 | case 0x48: return SI::write(pAddr, value); 369 | } 370 | } 371 | 372 | if (data != nullptr) 373 | { 374 | // Write a value to the pointer, big-endian style (MSB first) 375 | for (size_t i = 0; i < sizeof(T); i++) 376 | data[i] = value >> ((sizeof(T) - 1 - i) * 8); 377 | return; 378 | } 379 | 380 | LOG_WARN("Unknown memory write: 0x%X\n", address); 381 | } 382 | 383 | void Memory::writeFlash(uint32_t value) 384 | { 385 | // Handle a FLASH register write based on https://github.com/Dillonb/n64 386 | switch (uint8_t command = value >> 24) 387 | { 388 | case 0xD2: // Execute 389 | switch (state) 390 | { 391 | case FLASH_WRITE: 392 | // Copy the write buffer to a save block 393 | for (int i = 0; i < 0x80; i++) 394 | Core::writeSave(writeOfs + i, writeBuf[i]); 395 | return; 396 | 397 | case FLASH_ERASE: 398 | // Reset the contents of a save block 399 | for (int i = 0; i < 0x80; i++) 400 | Core::writeSave(eraseOfs + i, 0xFF); 401 | return; 402 | 403 | default: 404 | LOG_WARN("Executing FLASH in invalid state: %d\n", state); 405 | return; 406 | } 407 | 408 | case 0xE1: // Status 409 | // Change the FLASH state to status 410 | state = FLASH_STATUS; 411 | status = 0x1111800100C2001D; 412 | return; 413 | 414 | case 0xF0: // Read 415 | // Change the FLASH state to read 416 | state = FLASH_READ; 417 | status = 0x11118004F0000000; 418 | return; 419 | 420 | case 0xB4: // Write 421 | // Change the FLASH state to write 422 | state = FLASH_WRITE; 423 | return; 424 | 425 | case 0x78: // Erase 426 | // Change the FLASH state to erase 427 | state = FLASH_ERASE; 428 | status = 0x1111800800C2001D; 429 | return; 430 | 431 | case 0x4B: // Erase Offset 432 | // Set the address of the save block to erase 433 | eraseOfs = (value & 0xFFFF) << 7; 434 | return; 435 | 436 | case 0xA5: // Write Offset 437 | // Set the address of the save block to write 438 | writeOfs = (value & 0xFFFF) << 7; 439 | status = 0x1111800400C2001D; 440 | return; 441 | 442 | default: 443 | LOG_CRIT("Unknown FLASH command: 0x%02X\n", command); 444 | return; 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/desktop/ry_frame.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "ry_frame.h" 21 | #include "ry_canvas.h" 22 | #include "input_dialog.h" 23 | #include "save_dialog.h" 24 | #include "../core.h" 25 | #include "../pif.h" 26 | #include "../settings.h" 27 | 28 | enum FrameEvent 29 | { 30 | LOAD_ROM = 1, 31 | CHANGE_SAVE, 32 | QUIT, 33 | PAUSE, 34 | RESTART, 35 | STOP, 36 | INPUT_BINDINGS, 37 | FPS_LIMITER, 38 | EXPANSION_PAK, 39 | THREADED_RDP, 40 | TEX_FILTER, 41 | UPDATE_JOY 42 | }; 43 | 44 | wxBEGIN_EVENT_TABLE(ryFrame, wxFrame) 45 | EVT_MENU(LOAD_ROM, ryFrame::loadRom) 46 | EVT_MENU(CHANGE_SAVE, ryFrame::changeSave) 47 | EVT_MENU(QUIT, ryFrame::quit) 48 | EVT_MENU(PAUSE, ryFrame::pause) 49 | EVT_MENU(RESTART, ryFrame::restart) 50 | EVT_MENU(STOP, ryFrame::stop) 51 | EVT_MENU(INPUT_BINDINGS, ryFrame::inputSettings) 52 | EVT_MENU(FPS_LIMITER, ryFrame::toggleFpsLimit) 53 | EVT_MENU(EXPANSION_PAK, ryFrame::toggleExpanPak) 54 | EVT_MENU(THREADED_RDP, ryFrame::toggleThreadRdp) 55 | EVT_MENU(TEX_FILTER, ryFrame::toggleTexFilter) 56 | EVT_TIMER(UPDATE_JOY, ryFrame::updateJoystick) 57 | EVT_DROP_FILES(ryFrame::dropFiles) 58 | EVT_CLOSE(ryFrame::close) 59 | wxEND_EVENT_TABLE() 60 | 61 | ryFrame::ryFrame(std::string path): wxFrame(nullptr, wxID_ANY, "rokuyon") 62 | { 63 | // Set up the file menu 64 | fileMenu = new wxMenu(); 65 | fileMenu->Append(LOAD_ROM, "&Load ROM"); 66 | fileMenu->Append(CHANGE_SAVE, "&Change Save Type"); 67 | fileMenu->AppendSeparator(); 68 | fileMenu->Append(QUIT, "&Quit"); 69 | 70 | // Set up the system menu 71 | systemMenu = new wxMenu(); 72 | systemMenu->Append(PAUSE, "&Resume"); 73 | systemMenu->Append(RESTART, "&Restart"); 74 | systemMenu->Append(STOP, "&Stop"); 75 | updateMenu(); 76 | 77 | // Set up the settings menu 78 | wxMenu *settingsMenu = new wxMenu(); 79 | settingsMenu->Append(INPUT_BINDINGS, "&Input Bindings"); 80 | settingsMenu->AppendSeparator(); 81 | settingsMenu->AppendCheckItem(FPS_LIMITER, "&FPS Limiter"); 82 | settingsMenu->AppendCheckItem(EXPANSION_PAK, "&Expansion Pak"); 83 | settingsMenu->AppendSeparator(); 84 | settingsMenu->AppendCheckItem(THREADED_RDP, "&Threaded RDP"); 85 | settingsMenu->AppendCheckItem(TEX_FILTER, "&Texture Filter"); 86 | 87 | // Set the initial checkbox states 88 | settingsMenu->Check(FPS_LIMITER, Settings::fpsLimiter); 89 | settingsMenu->Check(EXPANSION_PAK, Settings::expansionPak); 90 | settingsMenu->Check(THREADED_RDP, Settings::threadedRdp); 91 | settingsMenu->Check(TEX_FILTER, Settings::texFilter); 92 | 93 | // Set up the menu bar 94 | wxMenuBar *menuBar = new wxMenuBar(); 95 | menuBar->Append(fileMenu, "&File"); 96 | menuBar->Append(systemMenu, "&System"); 97 | menuBar->Append(settingsMenu, "&Settings"); 98 | SetMenuBar(menuBar); 99 | 100 | // Set up and show the window 101 | DragAcceptFiles(true); 102 | SetClientSize(MIN_SIZE); 103 | SetMinClientSize(MIN_SIZE); 104 | Centre(); 105 | Show(true); 106 | 107 | // Set up a canvas for drawing the framebuffer 108 | canvas = new ryCanvas(this); 109 | wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); 110 | sizer->Add(canvas, 1, wxEXPAND); 111 | SetSizer(sizer); 112 | 113 | // Prepare a joystick if one is connected 114 | joystick = new wxJoystick(); 115 | if (joystick->IsOk()) 116 | { 117 | // Save the initial axis values so inputs can be detected as offsets instead of raw values 118 | // This avoids issues with axes that have non-zero values in their resting positions 119 | for (int i = 0; i < joystick->GetNumberAxes(); i++) 120 | axisBases.push_back(joystick->GetPosition(i)); 121 | 122 | // Start a timer to update joystick input, since wxJoystickEvents are unreliable 123 | timer = new wxTimer(this, UPDATE_JOY); 124 | timer->Start(10); 125 | } 126 | else 127 | { 128 | // Don't use a joystick if one isn't connected 129 | delete joystick; 130 | joystick = nullptr; 131 | timer = nullptr; 132 | } 133 | 134 | // Boot a ROM right away if a filename was given through the command line 135 | if (path != "") 136 | bootRom(path); 137 | } 138 | 139 | void ryFrame::bootRom(std::string path) 140 | { 141 | // Remember the path so the ROM can be restarted 142 | lastPath = path; 143 | 144 | // Try to boot the specified ROM, and display an error if failed 145 | if (!Core::bootRom(path)) 146 | { 147 | wxMessageDialog(this, "Make sure the ROM file is accessible and try again.", 148 | "Error Loading ROM", wxICON_NONE).ShowModal(); 149 | return; 150 | } 151 | 152 | // Reset the system menu 153 | paused = false; 154 | updateMenu(); 155 | } 156 | 157 | void ryFrame::updateMenu() 158 | { 159 | if (Core::running) 160 | { 161 | // Enable some menu items when the core is running 162 | systemMenu->SetLabel(PAUSE, "&Pause"); 163 | systemMenu->Enable(PAUSE, true); 164 | systemMenu->Enable(RESTART, true); 165 | systemMenu->Enable(STOP, true); 166 | fileMenu->Enable(CHANGE_SAVE, true); 167 | } 168 | else 169 | { 170 | // Disable some menu items when the core isn't running 171 | systemMenu->SetLabel(PAUSE, "&Resume"); 172 | if (!paused) 173 | { 174 | systemMenu->Enable(PAUSE, false); 175 | systemMenu->Enable(RESTART, false); 176 | systemMenu->Enable(STOP, false); 177 | fileMenu->Enable(CHANGE_SAVE, false); 178 | } 179 | } 180 | } 181 | 182 | void ryFrame::Refresh() 183 | { 184 | wxFrame::Refresh(); 185 | 186 | // Override the refresh function to also update the FPS counter 187 | wxString label = "rokuyon"; 188 | if (Core::running) 189 | label += wxString::Format(" - %d FPS", Core::fps); 190 | SetLabel(label); 191 | } 192 | 193 | void ryFrame::pressKey(int key) 194 | { 195 | if (key < 8) 196 | { 197 | // Press a key before the 2 unused keys 198 | PIF::pressKey(key); 199 | } 200 | else if (key < 14) 201 | { 202 | // Press a key after the 2 unused keys 203 | PIF::pressKey(key + 2); 204 | } 205 | else if (key < 19) 206 | { 207 | // Press a custom key-stick key 208 | stickPressed[key - 14] = true; 209 | updateKeyStick(); 210 | } 211 | } 212 | 213 | void ryFrame::releaseKey(int key) 214 | { 215 | if (key < 8) 216 | { 217 | // Release a key before the 2 unused keys 218 | PIF::releaseKey(key); 219 | } 220 | else if (key < 14) 221 | { 222 | // Release a key after the 2 unused keys 223 | PIF::releaseKey(key + 2); 224 | } 225 | else if (key < 19) 226 | { 227 | // Release a custom key-stick key 228 | stickPressed[key - 14] = false; 229 | updateKeyStick(); 230 | } 231 | } 232 | 233 | void ryFrame::updateKeyStick() 234 | { 235 | int stickX = 0; 236 | int stickY = 0; 237 | 238 | // Apply the base stick movement from pressed keys 239 | if (stickPressed[0]) stickY += 80; 240 | if (stickPressed[1]) stickY -= 80; 241 | if (stickPressed[2]) stickX -= 80; 242 | if (stickPressed[3]) stickX += 80; 243 | 244 | // Scale diagonals to create a round boundary 245 | if (stickX && stickY) 246 | { 247 | stickX = stickX * 60 / 80; 248 | stickY = stickY * 60 / 80; 249 | } 250 | 251 | // Half the coordinates when the stick modifier is applied 252 | if (stickPressed[4]) 253 | { 254 | stickX /= 2; 255 | stickY /= 2; 256 | } 257 | 258 | // Update the stick coordinates 259 | PIF::setStick(stickX, stickY); 260 | } 261 | 262 | void ryFrame::loadRom(wxCommandEvent &event) 263 | { 264 | // Show the file browser 265 | wxFileDialog romSelect(this, "Select ROM File", "", "", "N64 ROM files (*.z64)|*.z64", wxFD_OPEN | wxFD_FILE_MUST_EXIST); 266 | 267 | // Boot a ROM if a file was selected 268 | if (romSelect.ShowModal() != wxID_CANCEL) 269 | bootRom((const char*)romSelect.GetPath().mb_str(wxConvUTF8)); 270 | } 271 | 272 | void ryFrame::changeSave(wxCommandEvent &event) 273 | { 274 | // Show the save type dialog 275 | SaveDialog saveDialog(lastPath); 276 | saveDialog.ShowModal(); 277 | } 278 | 279 | void ryFrame::quit(wxCommandEvent &event) 280 | { 281 | // Close the program 282 | Close(true); 283 | } 284 | 285 | void ryFrame::pause(wxCommandEvent &event) 286 | { 287 | // Temporarily stop or start the emulator 288 | if ((paused = !paused)) 289 | Core::stop(); 290 | else 291 | Core::start(); 292 | updateMenu(); 293 | } 294 | 295 | void ryFrame::restart(wxCommandEvent &event) 296 | { 297 | // Boot the most recently loaded ROM again 298 | ryFrame::bootRom(lastPath); 299 | } 300 | 301 | void ryFrame::stop(wxCommandEvent &event) 302 | { 303 | // Stop the emulator and reset the system menu 304 | Core::stop(); 305 | paused = false; 306 | updateMenu(); 307 | } 308 | 309 | void ryFrame::inputSettings(wxCommandEvent &event) 310 | { 311 | // Pause joystick updates and show the input settings dialog 312 | if (timer) timer->Stop(); 313 | InputDialog inputDialog(joystick); 314 | inputDialog.ShowModal(); 315 | if (timer) timer->Start(10); 316 | } 317 | 318 | void ryFrame::toggleFpsLimit(wxCommandEvent &event) 319 | { 320 | // Toggle the FPS limiter setting 321 | Settings::fpsLimiter = !Settings::fpsLimiter; 322 | Settings::save(); 323 | } 324 | 325 | void ryFrame::toggleExpanPak(wxCommandEvent &event) 326 | { 327 | // Toggle the Expansion Pak setting 328 | Settings::expansionPak = !Settings::expansionPak; 329 | Settings::save(); 330 | } 331 | 332 | void ryFrame::toggleThreadRdp(wxCommandEvent &event) 333 | { 334 | // Toggle the threaded RDP setting 335 | Settings::threadedRdp = !Settings::threadedRdp; 336 | Settings::save(); 337 | } 338 | 339 | void ryFrame::toggleTexFilter(wxCommandEvent &event) 340 | { 341 | // Toggle the texture filter setting 342 | Settings::texFilter = !Settings::texFilter; 343 | Settings::save(); 344 | } 345 | 346 | void ryFrame::updateJoystick(wxTimerEvent &event) 347 | { 348 | int stickX = 0; 349 | int stickY = 0; 350 | 351 | // Check the status of mapped joystick inputs 352 | for (int i = 0; i < MAX_KEYS; i++) 353 | { 354 | if (ryApp::keyBinds[i] >= 3000 && joystick->GetNumberAxes() > ryApp::keyBinds[i] - 3000) // Axis - 355 | { 356 | int j = ryApp::keyBinds[i] - 3000; 357 | switch (i) 358 | { 359 | case 14: // Stick Up 360 | // Scale the axis position and apply it to the stick in the up direction 361 | if (joystick->GetPosition(j) < axisBases[j]) 362 | stickY += (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetYMin(); 363 | continue; 364 | 365 | case 15: // Stick Down 366 | // Scale the axis position and apply it to the stick in the down direction 367 | if (joystick->GetPosition(j) < axisBases[j]) 368 | stickY -= (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetYMax(); 369 | continue; 370 | 371 | case 16: // Stick Left 372 | // Scale the axis position and apply it to the stick in the left direction 373 | if (joystick->GetPosition(j) < axisBases[j]) 374 | stickX -= (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetXMin(); 375 | continue; 376 | 377 | case 17: // Stick Right 378 | // Scale the axis position and apply it to the stick in the right direction 379 | if (joystick->GetPosition(j) < axisBases[j]) 380 | stickX += (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetXMax(); 381 | continue; 382 | 383 | default: 384 | // Trigger a key press or release based on the axis position 385 | if (joystick->GetPosition(j) < axisBases[j] - joystick->GetXMax() / 2) 386 | pressKey(i); 387 | else 388 | releaseKey(i); 389 | continue; 390 | } 391 | } 392 | else if (ryApp::keyBinds[i] >= 2000 && joystick->GetNumberAxes() > ryApp::keyBinds[i] - 2000) // Axis + 393 | { 394 | int j = ryApp::keyBinds[i] - 2000; 395 | switch (i) 396 | { 397 | case 14: // Stick Up 398 | // Scale the axis position and apply it to the stick in the up direction 399 | if (joystick->GetPosition(j) > axisBases[j]) 400 | stickY += (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetYMin(); 401 | continue; 402 | 403 | case 15: // Stick Down 404 | // Scale the axis position and apply it to the stick in the down direction 405 | if (joystick->GetPosition(j) > axisBases[j]) 406 | stickY -= (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetYMax(); 407 | continue; 408 | 409 | case 16: // Stick Left 410 | // Scale the axis position and apply it to the stick in the left direction 411 | if (joystick->GetPosition(j) > axisBases[j]) 412 | stickX -= (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetXMin(); 413 | continue; 414 | 415 | case 17: // Stick Right 416 | // Scale the axis position and apply it to the stick in the right direction 417 | if (joystick->GetPosition(j) > axisBases[j]) 418 | stickX += (joystick->GetPosition(j) - axisBases[j]) * 80 / joystick->GetXMax(); 419 | continue; 420 | 421 | default: 422 | // Trigger a key press or release based on the axis position 423 | if (joystick->GetPosition(j) > axisBases[j] + joystick->GetXMax() / 2) 424 | pressKey(i); 425 | else 426 | releaseKey(i); 427 | continue; 428 | } 429 | } 430 | else if (ryApp::keyBinds[i] >= 1000 && joystick->GetNumberButtons() > ryApp::keyBinds[i] - 1000) // Button 431 | { 432 | // Trigger a key press or release based on the button status 433 | if (joystick->GetButtonState(ryApp::keyBinds[i] - 1000)) 434 | pressKey(i); 435 | else 436 | releaseKey(i); 437 | } 438 | } 439 | 440 | // Half the coordinates when the stick modifier is applied 441 | if (stickPressed[4]) 442 | { 443 | stickX /= 2; 444 | stickY /= 2; 445 | } 446 | 447 | // Update the stick coordinates if key-stick is inactive 448 | if (!(stickPressed[0] | stickPressed[1] | stickPressed[2] | stickPressed[3])) 449 | PIF::setStick(stickX, stickY); 450 | } 451 | 452 | void ryFrame::dropFiles(wxDropFilesEvent &event) 453 | { 454 | // Boot a ROM if a single file is dropped onto the frame 455 | if (event.GetNumberOfFiles() == 1) 456 | { 457 | wxString path = event.GetFiles()[0]; 458 | if (wxFileExists(path)) 459 | bootRom((const char*)path.mb_str(wxConvUTF8)); 460 | } 461 | } 462 | 463 | void ryFrame::close(wxCloseEvent &event) 464 | { 465 | // Stop emulation before exiting 466 | Core::stop(); 467 | canvas->finish(); 468 | event.Skip(true); 469 | } 470 | -------------------------------------------------------------------------------- /src/desktop/input_dialog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 Hydr8gon 3 | 4 | This file is part of rokuyon. 5 | 6 | rokuyon is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | rokuyon is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with rokuyon. If not, see . 18 | */ 19 | 20 | #include "input_dialog.h" 21 | #include "../settings.h" 22 | 23 | enum InputEvent 24 | { 25 | REMAP_A = 1, 26 | REMAP_B, 27 | REMAP_Z, 28 | REMAP_START, 29 | REMAP_DUP, 30 | REMAP_DDOWN, 31 | REMAP_DLEFT, 32 | REMAP_DRIGHT, 33 | REMAP_L, 34 | REMAP_R, 35 | REMAP_CUP, 36 | REMAP_CDOWN, 37 | REMAP_CLEFT, 38 | REMAP_CRIGHT, 39 | REMAP_SUP, 40 | REMAP_SDOWN, 41 | REMAP_SLEFT, 42 | REMAP_SRIGHT, 43 | REMAP_SMOD, 44 | REMAP_FULLSCREEN, 45 | CLEAR_MAP, 46 | UPDATE_JOY 47 | }; 48 | 49 | wxBEGIN_EVENT_TABLE(InputDialog, wxDialog) 50 | EVT_BUTTON(REMAP_A, InputDialog::remapA) 51 | EVT_BUTTON(REMAP_B, InputDialog::remapB) 52 | EVT_BUTTON(REMAP_Z, InputDialog::remapZ) 53 | EVT_BUTTON(REMAP_START, InputDialog::remapStart) 54 | EVT_BUTTON(REMAP_DUP, InputDialog::remapDUp) 55 | EVT_BUTTON(REMAP_DDOWN, InputDialog::remapDDown) 56 | EVT_BUTTON(REMAP_DLEFT, InputDialog::remapDLeft) 57 | EVT_BUTTON(REMAP_DRIGHT, InputDialog::remapDRight) 58 | EVT_BUTTON(REMAP_L, InputDialog::remapL) 59 | EVT_BUTTON(REMAP_R, InputDialog::remapR) 60 | EVT_BUTTON(REMAP_CUP, InputDialog::remapCUp) 61 | EVT_BUTTON(REMAP_CDOWN, InputDialog::remapCDown) 62 | EVT_BUTTON(REMAP_CLEFT, InputDialog::remapCLeft) 63 | EVT_BUTTON(REMAP_CRIGHT, InputDialog::remapCRight) 64 | EVT_BUTTON(REMAP_SUP, InputDialog::remapSUp) 65 | EVT_BUTTON(REMAP_SDOWN, InputDialog::remapSDown) 66 | EVT_BUTTON(REMAP_SLEFT, InputDialog::remapSLeft) 67 | EVT_BUTTON(REMAP_SRIGHT, InputDialog::remapSRight) 68 | EVT_BUTTON(REMAP_SMOD, InputDialog::remapSMod) 69 | EVT_BUTTON(REMAP_FULLSCREEN, InputDialog::remapFullScreen) 70 | EVT_BUTTON(CLEAR_MAP, InputDialog::clearMap) 71 | EVT_TIMER(UPDATE_JOY, InputDialog::updateJoystick) 72 | EVT_BUTTON(wxID_OK, InputDialog::confirm) 73 | EVT_CHAR_HOOK(InputDialog::pressKey) 74 | wxEND_EVENT_TABLE() 75 | 76 | std::string InputDialog::keyToString(int key) 77 | { 78 | // Handle joystick keys based on the special offsets assigned to them 79 | if (key >= 3000) 80 | return "Axis " + std::to_string(key - 3000) + " -"; 81 | else if (key >= 2000) 82 | return "Axis " + std::to_string(key - 2000) + " +"; 83 | else if (key >= 1000) 84 | return "Button " + std::to_string(key - 1000); 85 | 86 | // Convert special keys to words representing their respective keys 87 | switch (key) 88 | { 89 | case 0: return "None"; 90 | case WXK_BACK: return "Backspace"; 91 | case WXK_TAB: return "Tab"; 92 | case WXK_RETURN: return "Return"; 93 | case WXK_ESCAPE: return "Escape"; 94 | case WXK_SPACE: return "Space"; 95 | case WXK_DELETE: return "Delete"; 96 | case WXK_START: return "Start"; 97 | case WXK_LBUTTON: return "Left Button"; 98 | case WXK_RBUTTON: return "Right Button"; 99 | case WXK_CANCEL: return "Cancel"; 100 | case WXK_MBUTTON: return "Middle Button"; 101 | case WXK_CLEAR: return "Clear"; 102 | case WXK_SHIFT: return "Shift"; 103 | case WXK_ALT: return "Alt"; 104 | case WXK_RAW_CONTROL: return "Control"; 105 | case WXK_MENU: return "Menu"; 106 | case WXK_PAUSE: return "Pause"; 107 | case WXK_CAPITAL: return "Caps Lock"; 108 | case WXK_END: return "End"; 109 | case WXK_HOME: return "Home"; 110 | case WXK_LEFT: return "Left"; 111 | case WXK_UP: return "Up"; 112 | case WXK_RIGHT: return "Right"; 113 | case WXK_DOWN: return "Down"; 114 | case WXK_SELECT: return "Select"; 115 | case WXK_PRINT: return "Print"; 116 | case WXK_EXECUTE: return "Execute"; 117 | case WXK_SNAPSHOT: return "Snapshot"; 118 | case WXK_INSERT: return "Insert"; 119 | case WXK_HELP: return "Help"; 120 | case WXK_NUMPAD0: return "Numpad 0"; 121 | case WXK_NUMPAD1: return "Numpad 1"; 122 | case WXK_NUMPAD2: return "Numpad 2"; 123 | case WXK_NUMPAD3: return "Numpad 3"; 124 | case WXK_NUMPAD4: return "Numpad 4"; 125 | case WXK_NUMPAD5: return "Numpad 5"; 126 | case WXK_NUMPAD6: return "Numpad 6"; 127 | case WXK_NUMPAD7: return "Numpad 7"; 128 | case WXK_NUMPAD8: return "Numpad 8"; 129 | case WXK_NUMPAD9: return "Numpad 9"; 130 | case WXK_MULTIPLY: return "Multiply"; 131 | case WXK_ADD: return "Add"; 132 | case WXK_SEPARATOR: return "Separator"; 133 | case WXK_SUBTRACT: return "Subtract"; 134 | case WXK_DECIMAL: return "Decimal"; 135 | case WXK_DIVIDE: return "Divide"; 136 | case WXK_F1: return "F1"; 137 | case WXK_F2: return "F2"; 138 | case WXK_F3: return "F3"; 139 | case WXK_F4: return "F4"; 140 | case WXK_F5: return "F5"; 141 | case WXK_F6: return "F6"; 142 | case WXK_F7: return "F7"; 143 | case WXK_F8: return "F8"; 144 | case WXK_F9: return "F9"; 145 | case WXK_F10: return "F10"; 146 | case WXK_F11: return "F11"; 147 | case WXK_F12: return "F12"; 148 | case WXK_F13: return "F13"; 149 | case WXK_F14: return "F14"; 150 | case WXK_F15: return "F15"; 151 | case WXK_F16: return "F16"; 152 | case WXK_F17: return "F17"; 153 | case WXK_F18: return "F18"; 154 | case WXK_F19: return "F19"; 155 | case WXK_F20: return "F20"; 156 | case WXK_F21: return "F21"; 157 | case WXK_F22: return "F22"; 158 | case WXK_F23: return "F23"; 159 | case WXK_F24: return "F24"; 160 | case WXK_NUMLOCK: return "Numlock"; 161 | case WXK_SCROLL: return "Scroll"; 162 | case WXK_PAGEUP: return "Page Up"; 163 | case WXK_PAGEDOWN: return "Page Down"; 164 | case WXK_NUMPAD_SPACE: return "Numpad Space"; 165 | case WXK_NUMPAD_TAB: return "Numpad Tab"; 166 | case WXK_NUMPAD_ENTER: return "Numpad Enter"; 167 | case WXK_NUMPAD_F1: return "Numpad F1"; 168 | case WXK_NUMPAD_F2: return "Numpad F2"; 169 | case WXK_NUMPAD_F3: return "Numpad F3"; 170 | case WXK_NUMPAD_F4: return "Numpad F4"; 171 | case WXK_NUMPAD_HOME: return "Numpad Home"; 172 | case WXK_NUMPAD_LEFT: return "Numpad Left"; 173 | case WXK_NUMPAD_UP: return "Numpad Up"; 174 | case WXK_NUMPAD_RIGHT: return "Numpad Right"; 175 | case WXK_NUMPAD_DOWN: return "Numpad Down"; 176 | case WXK_NUMPAD_PAGEUP: return "Numpad Page Up"; 177 | case WXK_NUMPAD_PAGEDOWN: return "Numpad Page Down"; 178 | case WXK_NUMPAD_END: return "Numpad End"; 179 | case WXK_NUMPAD_BEGIN: return "Numpad Begin"; 180 | case WXK_NUMPAD_INSERT: return "Numpad Insert"; 181 | case WXK_NUMPAD_DELETE: return "Numpad Delete"; 182 | case WXK_NUMPAD_EQUAL: return "Numpad Equal"; 183 | case WXK_NUMPAD_MULTIPLY: return "Numpad Multiply"; 184 | case WXK_NUMPAD_ADD: return "Numpad Add"; 185 | case WXK_NUMPAD_SEPARATOR: return "Numpad Separator"; 186 | case WXK_NUMPAD_SUBTRACT: return "Numpad Subtract"; 187 | case WXK_NUMPAD_DECIMAL: return "Numpad Decimal"; 188 | case WXK_NUMPAD_DIVIDE: return "Numpad Divide"; 189 | } 190 | 191 | // Directly use the key character for regular keys 192 | std::string regular; 193 | regular = (char)key; 194 | return regular; 195 | } 196 | 197 | InputDialog::InputDialog(wxJoystick *joystick): 198 | wxDialog(nullptr, wxID_ANY, "Input Bindings"), joystick(joystick) 199 | { 200 | // Get the height of a button in pixels as a reference scale for the rest of the UI 201 | wxButton *dummy = new wxButton(this, wxID_ANY, ""); 202 | size_t scale = dummy->GetSize().y; 203 | delete dummy; 204 | 205 | // Load the current key bindings 206 | memcpy(keyBinds, ryApp::keyBinds, sizeof(keyBinds)); 207 | 208 | // Define labels for the bindings 209 | static const std::string labels[] = 210 | { 211 | "A Button", "B Button", "Z Button", "Start Button", 212 | "D-Pad Up", "D-Pad Down", "D-Pad Left", "D-Pad Right", 213 | "L Button", "R Button", 214 | "C-Pad Up", "C-Pad Down", "C-Pad Left", "C-Pad Right", 215 | "Stick Up", "Stick Down", "Stick Left", "Stick Right", 216 | "Stick Mod", "Full Screen" 217 | }; 218 | 219 | // Set up individual buttons for each binding 220 | wxBoxSizer *keySizers[MAX_KEYS]; 221 | for (int i = 0; i < MAX_KEYS; i++) 222 | { 223 | keySizers[i] = new wxBoxSizer(wxHORIZONTAL); 224 | keySizers[i]->Add(new wxStaticText(this, wxID_ANY, labels[i] + ":"), 1, wxALIGN_CENTRE | wxRIGHT, scale / 16); 225 | keys[i] = new wxButton(this, REMAP_A + i, keyToString(keyBinds[i]), wxDefaultPosition, wxSize(scale * 4, scale)); 226 | keySizers[i]->Add(keys[i], 0, wxLEFT, scale / 16); 227 | } 228 | 229 | // Add buttons to the first column of the layout 230 | wxBoxSizer *column1 = new wxBoxSizer(wxVERTICAL); 231 | column1->Add(keySizers[14], 1, wxEXPAND | wxALL, scale / 8); 232 | column1->Add(keySizers[15], 1, wxEXPAND | wxALL, scale / 8); 233 | column1->Add(keySizers[16], 1, wxEXPAND | wxALL, scale / 8); 234 | column1->Add(keySizers[17], 1, wxEXPAND | wxALL, scale / 8); 235 | column1->Add(keySizers[18], 1, wxEXPAND | wxALL, scale / 8); 236 | 237 | // Add buttons to the second column of the layout 238 | wxBoxSizer *column2 = new wxBoxSizer(wxVERTICAL); 239 | column2->Add(keySizers[0], 1, wxEXPAND | wxALL, scale / 8); 240 | column2->Add(keySizers[1], 1, wxEXPAND | wxALL, scale / 8); 241 | column2->Add(keySizers[2], 1, wxEXPAND | wxALL, scale / 8); 242 | column2->Add(keySizers[3], 1, wxEXPAND | wxALL, scale / 8); 243 | column2->Add(keySizers[8], 1, wxEXPAND | wxALL, scale / 8); 244 | 245 | // Add buttons to the third column of the layout 246 | wxBoxSizer *column3 = new wxBoxSizer(wxVERTICAL); 247 | column3->Add(keySizers[10], 1, wxEXPAND | wxALL, scale / 8); 248 | column3->Add(keySizers[11], 1, wxEXPAND | wxALL, scale / 8); 249 | column3->Add(keySizers[12], 1, wxEXPAND | wxALL, scale / 8); 250 | column3->Add(keySizers[13], 1, wxEXPAND | wxALL, scale / 8); 251 | column3->Add(keySizers[9], 1, wxEXPAND | wxALL, scale / 8); 252 | 253 | // Add buttons to the fourth column of the layout 254 | wxBoxSizer *column4 = new wxBoxSizer(wxVERTICAL); 255 | column4->Add(keySizers[4], 1, wxEXPAND | wxALL, scale / 8); 256 | column4->Add(keySizers[5], 1, wxEXPAND | wxALL, scale / 8); 257 | column4->Add(keySizers[6], 1, wxEXPAND | wxALL, scale / 8); 258 | column4->Add(keySizers[7], 1, wxEXPAND | wxALL, scale / 8); 259 | column4->Add(keySizers[19], 1, wxEXPAND | wxALL, scale / 8); 260 | 261 | // Combine the button tab contents and add a final border around it 262 | wxBoxSizer *buttonSizer = new wxBoxSizer(wxHORIZONTAL); 263 | buttonSizer->Add(column1, 1, wxEXPAND | wxALL, scale / 8); 264 | buttonSizer->Add(column2, 1, wxEXPAND | wxALL, scale / 8); 265 | buttonSizer->Add(column3, 1, wxEXPAND | wxALL, scale / 8); 266 | buttonSizer->Add(column4, 1, wxEXPAND | wxALL, scale / 8); 267 | 268 | // Set up the navigation buttons 269 | wxBoxSizer *naviSizer = new wxBoxSizer(wxHORIZONTAL); 270 | naviSizer->Add(new wxStaticText(this, wxID_ANY, ""), 1); 271 | naviSizer->Add(new wxButton(this, CLEAR_MAP, "Clear"), 0, wxRIGHT, scale / 16); 272 | naviSizer->Add(new wxButton(this, wxID_CANCEL, "Cancel"), 0, wxLEFT | wxRIGHT, scale / 16); 273 | naviSizer->Add(new wxButton(this, wxID_OK, "Confirm"), 0, wxLEFT, scale / 16); 274 | 275 | // Populate the dialog 276 | wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); 277 | sizer->Add(buttonSizer, 1, wxEXPAND); 278 | sizer->Add(naviSizer, 0, wxEXPAND | wxALL, scale / 8); 279 | SetSizerAndFit(sizer); 280 | 281 | // Lock the window to the default size 282 | SetMinSize(GetSize()); 283 | SetMaxSize(GetSize()); 284 | 285 | // Set up joystick input if a joystick is connected 286 | if (joystick) 287 | { 288 | // Save the initial axis values so inputs can be detected as offsets instead of raw values 289 | // This avoids issues with axes that have non-zero values in their resting positions 290 | for (int i = 0; i < joystick->GetNumberAxes(); i++) 291 | axisBases.push_back(joystick->GetPosition(i)); 292 | 293 | // Start a timer to update joystick input, since wxJoystickEvents are unreliable 294 | timer = new wxTimer(this, UPDATE_JOY); 295 | timer->Start(10); 296 | } 297 | } 298 | 299 | InputDialog::~InputDialog() 300 | { 301 | // Clean up the joystick timer 302 | if (joystick) 303 | delete timer; 304 | } 305 | 306 | void InputDialog::resetLabels() 307 | { 308 | // Reset the button labels 309 | for (int i = 0; i < MAX_KEYS; i++) 310 | keys[i]->SetLabel(keyToString(keyBinds[i])); 311 | current = nullptr; 312 | } 313 | 314 | // Prepare an input binding for remapping 315 | #define REMAP_FUNC(name, index) \ 316 | void InputDialog::name(wxCommandEvent &event) \ 317 | { \ 318 | resetLabels(); \ 319 | keys[index]->SetLabel("Press a key"); \ 320 | current = keys[index]; \ 321 | keyIndex = index; \ 322 | } 323 | 324 | REMAP_FUNC(remapA, 0) 325 | REMAP_FUNC(remapB, 1) 326 | REMAP_FUNC(remapZ, 2) 327 | REMAP_FUNC(remapStart, 3) 328 | REMAP_FUNC(remapDUp, 4) 329 | REMAP_FUNC(remapDDown, 5) 330 | REMAP_FUNC(remapDLeft, 6) 331 | REMAP_FUNC(remapDRight, 7) 332 | REMAP_FUNC(remapL, 8) 333 | REMAP_FUNC(remapR, 9) 334 | REMAP_FUNC(remapCUp, 10) 335 | REMAP_FUNC(remapCDown, 11) 336 | REMAP_FUNC(remapCLeft, 12) 337 | REMAP_FUNC(remapCRight, 13) 338 | REMAP_FUNC(remapSUp, 14) 339 | REMAP_FUNC(remapSDown, 15) 340 | REMAP_FUNC(remapSLeft, 16) 341 | REMAP_FUNC(remapSRight, 17) 342 | REMAP_FUNC(remapSMod, 18) 343 | REMAP_FUNC(remapFullScreen, 19) 344 | 345 | void InputDialog::clearMap(wxCommandEvent &event) 346 | { 347 | if (current) 348 | { 349 | // If a button is selected, clear only its mapping 350 | keyBinds[keyIndex] = 0; 351 | current->SetLabel(keyToString(keyBinds[keyIndex])); 352 | current = nullptr; 353 | } 354 | else 355 | { 356 | // If no button is selected, clear all mappings 357 | for (int i = 0; i < MAX_KEYS; i++) 358 | keyBinds[i] = 0; 359 | resetLabels(); 360 | } 361 | } 362 | 363 | void InputDialog::updateJoystick(wxTimerEvent &event) 364 | { 365 | if (!current) return; 366 | 367 | // Map the current button to a joystick button if one is pressed 368 | for (int i = 0; i < joystick->GetNumberButtons(); i++) 369 | { 370 | if (joystick->GetButtonState(i)) 371 | { 372 | keyBinds[keyIndex] = 1000 + i; 373 | current->SetLabel(keyToString(keyBinds[keyIndex])); 374 | current = nullptr; 375 | return; 376 | } 377 | } 378 | 379 | // Map the current button to a joystick axis if one is pushed far enough 380 | for (int i = 0; i < joystick->GetNumberAxes(); i++) 381 | { 382 | if (joystick->GetPosition(i) - axisBases[i] > joystick->GetXMax() / 2) // Positive axis 383 | { 384 | keyBinds[keyIndex] = 2000 + i; 385 | current->SetLabel(keyToString(keyBinds[keyIndex])); 386 | current = nullptr; 387 | return; 388 | } 389 | else if (joystick->GetPosition(i) - axisBases[i] < joystick->GetXMin() / 2) // Negative axis 390 | { 391 | keyBinds[keyIndex] = 3000 + i; 392 | current->SetLabel(keyToString(keyBinds[keyIndex])); 393 | current = nullptr; 394 | return; 395 | } 396 | } 397 | } 398 | 399 | void InputDialog::confirm(wxCommandEvent &event) 400 | { 401 | // Update and save the key bindings 402 | memcpy(ryApp::keyBinds, keyBinds, sizeof(keyBinds)); 403 | Settings::save(); 404 | event.Skip(true); 405 | } 406 | 407 | void InputDialog::pressKey(wxKeyEvent &event) 408 | { 409 | // Map the selected button to the pressed key 410 | if (current) 411 | { 412 | keyBinds[keyIndex] = event.GetKeyCode(); 413 | current->SetLabel(keyToString(keyBinds[keyIndex])); 414 | current = nullptr; 415 | } 416 | } 417 | --------------------------------------------------------------------------------