├── .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 |
--------------------------------------------------------------------------------