From 553e0e6745a4f357b0705a97bb19e7c7822eb4f7 Mon Sep 17 00:00:00 2001 From: kishimisu Date: Thu, 16 Nov 2023 01:09:46 +0100 Subject: [PATCH] add browser, inputs & display --- .gitignore | 8 +- CMakeLists.txt | 15 - CMakePresets.json | 61 ---- JJSH3.g1a | Bin 0 -> 135992 bytes PrintMini.bmp | Bin 0 -> 2110 bytes README.md | 32 +- .../fx9860-emulator/U+0020.png => U+0020.png | Bin fx9860-emulator/headers/display.h | 7 +- .../headers/instructions/instructions.h | 2 + .../headers/instructions/syscalls.h | 12 +- fx9860-emulator/headers/main.h | 38 +- fx9860-emulator/headers/memory.h | 32 +- fx9860-emulator/headers/utils.h | 6 +- fx9860-emulator/instructions/instructions.c | 7 +- fx9860-emulator/instructions/syscalls.c | 327 +++++++++++++++--- fx9860-emulator/main.c | 101 ++++-- fx9860-emulator/memory.c | 127 +++++-- fx9860-emulator/utils.c | 96 +++-- .../fx9860-emulator/SAMPLE_ADDIN.G1A | Bin 1684 -> 0 bytes 19 files changed, 608 insertions(+), 263 deletions(-) delete mode 100644 CMakeLists.txt delete mode 100644 CMakePresets.json create mode 100644 JJSH3.g1a create mode 100644 PrintMini.bmp rename out/build/x64-debug/fx9860-emulator/U+0020.png => U+0020.png (100%) delete mode 100644 out/build/x64-debug/fx9860-emulator/SAMPLE_ADDIN.G1A diff --git a/.gitignore b/.gitignore index a5c4883..6489b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ .vs -out/build/x64-debug/* -!out/build/x64-debug/fx9860-emulator -out/build/x64-debug/fx9860-emulator/* -!out/build/x64-debug/fx9860-emulator/SAMPLE_ADDIN.G1A -!out/build/x64-debug/fx9860-emulator/U+0020.png +.vscode +tmp/ +fx9860-emulator/build \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 25f4f35..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# CMakeList.txt : Top-level CMake project file, do global configuration -# and include sub-projects here. -# -cmake_minimum_required (VERSION 3.8) - -# Enable Hot Reload for MSVC compilers if supported. -if (POLICY CMP0141) - cmake_policy(SET CMP0141 NEW) - set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$,$>,$<$:EditAndContinue>,$<$:ProgramDatabase>>") -endif() - -project ("fx9860-emulator") - -# Include sub-projects. -add_subdirectory ("fx9860-emulator") diff --git a/CMakePresets.json b/CMakePresets.json deleted file mode 100644 index abf4065..0000000 --- a/CMakePresets.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "version": 3, - "configurePresets": [ - { - "name": "windows-base", - "hidden": true, - "generator": "Ninja", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "installDir": "${sourceDir}/out/install/${presetName}", - "cacheVariables": { - "CMAKE_C_COMPILER": "cl.exe", - "CMAKE_CXX_COMPILER": "cl.exe" - }, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } - }, - { - "name": "x64-debug", - "displayName": "x64 Debug", - "inherits": "windows-base", - "architecture": { - "value": "x64", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "x64-release", - "displayName": "x64 Release", - "inherits": "x64-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "x86-debug", - "displayName": "x86 Debug", - "inherits": "windows-base", - "architecture": { - "value": "x86", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "x86-release", - "displayName": "x86 Release", - "inherits": "x86-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - } - ] -} diff --git a/JJSH3.g1a b/JJSH3.g1a new file mode 100644 index 0000000000000000000000000000000000000000..c5978191a15ce1d1aa0951c209d06775e5df7255 GIT binary patch literal 135992 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

JzmAR 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 AumAu6 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index ff18bd9..85ae94c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ This is a work-in-progress emulator for the Casio fx-9860 SH4 models It will display the content of the VRAM in the console at the end of the execution. +Broswser demo : https://sh4.vercel.app/ + All non-DSP instructions are implemented, except the following ones: - 'MOVUAL', 'MOVUALP', 'ICBI', 'OCBI', - 'OCBP', 'OCBWB', 'PREFI', 'SLEEP', @@ -10,4 +12,32 @@ All non-DSP instructions are implemented, except the following ones: fx9860-emulator/ contains the source for the emulator -scripts/ contains the code for auto-generating the instructions code from the doc \ No newline at end of file +scripts/ contains the code for auto-generating the instructions code from the doc + +### Browser version +To build the browser version of the emulator, make sure you have emscripten downloaded and installed: https://emscripten.org/docs/getting_started/downloads.html + +First, make sur `#define EMSCRIPT` is enabled in main.h (not as a comment). +Then: + + cd fx9860-emulator + mkdir build + cd build + emcmake make .. + +Then, until I find a nicer way to integrate this into the Makefile, run the following command: + + emcc -O2 .\CMakeFiles\fx9860-emulator.dir\main.o .\CMakeFiles\fx9860-emulator.dir\utils.o .\CMakeFiles\fx9860-emulator.dir\memory.o .\CMakeFiles\fx9860-emulator.dir\instructions\instructions.o .\CMakeFiles\fx9860-emulator.dir\instructions\syscalls.o --embed-file ..\..\U+0020.png@U+0020.png --embed-file ..\..\PrintMini.bmp@PrintMini.bmp --embed-file ..\..\JJSH3.G1A@JJSH3.G1A -sASYNCIFY -sEXIT_RUNTIME -o index.js -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$getBoundingClientRect' -sEXPORTED_RUNTIME_METHODS='writeArrayToMemory, setValue, getValue' + +After that you can server the content of the build/ folder through a local server and open index.html + +### Console version +To build the console version of the emulator (no display, only VRAM dump after 10M executions): + +First, remove (comment) `#define EMSCRIPT` in main.h + + cd fx9860-emulator + mkdir build + cd build + cmake .. + make \ No newline at end of file diff --git a/out/build/x64-debug/fx9860-emulator/U+0020.png b/U+0020.png similarity index 100% rename from out/build/x64-debug/fx9860-emulator/U+0020.png rename to U+0020.png diff --git a/fx9860-emulator/headers/display.h b/fx9860-emulator/headers/display.h index ea1f655..3a6b6a7 100644 --- a/fx9860-emulator/headers/display.h +++ b/fx9860-emulator/headers/display.h @@ -26,16 +26,15 @@ typedef struct { int col; // starts at 1 int row; // starts at 1 - //int flashstyle; // -1 if not flashing, else flashmode - //int graphic_mode; } cursor_t; struct display_t { // Pointer to virtual memory in RAM uint8_t vram[VRAM_SIZE]; - //uint8_t* vram; - uint8_t lcd_registers; // Unused, but still emulates access to the LCD screen + // LCD screen emulation + uint8_t lcd[VRAM_SIZE]; + uint8_t lcd_registers; // Use for both selector & data registers // Current cursor position cursor_t cursor; diff --git a/fx9860-emulator/headers/instructions/instructions.h b/fx9860-emulator/headers/instructions/instructions.h index 2986f5a..eef9e42 100644 --- a/fx9860-emulator/headers/instructions/instructions.h +++ b/fx9860-emulator/headers/instructions/instructions.h @@ -4,6 +4,8 @@ void (*get_instruction_impl(uint16_t instruction))(cpu_t*, uint16_t); +#define R0 cpu->r[0] + void ADD(cpu_t* cpu, uint16_t instruction); void ADDI(cpu_t* cpu, uint16_t instruction); void ADDC(cpu_t* cpu, uint16_t instruction); diff --git a/fx9860-emulator/headers/instructions/syscalls.h b/fx9860-emulator/headers/instructions/syscalls.h index 4211b22..78ff512 100644 --- a/fx9860-emulator/headers/instructions/syscalls.h +++ b/fx9860-emulator/headers/instructions/syscalls.h @@ -9,9 +9,17 @@ void syscall_Bdisp_AllClr_DD(cpu_t* cpu); void syscall_Bdisp_AllClr_VRAM(cpu_t* cpu); void syscall_Bdisp_AllClr_DDVRAM(cpu_t* cpu); +void syscall_GetKey(cpu_t* cpu, unsigned int keycode_address); + void syscall_Locate(cpu_t* cpu, int x, int y); void syscall_Print(cpu_t* cpu, const unsigned char* str); -void syscall_PrintXY(cpu_t* cpu, int x, int y, const unsigned char* str, int type); +void syscall_PrintXY(cpu_t* cpu, int x, int y, const unsigned char* str, int mode); +void syscall_PrintMiniSd(cpu_t* cpu, int x, int y, const unsigned char* str, int mode); void syscall_GetAppName(cpu_t* cpu, char* dest); -void syscall_Malloc(cpu_t* cpu, uint32_t size); \ No newline at end of file +void syscall_GlibGetAddinLibInf(cpu_t* cpu, uint32_t a_ptr, uint32_t b_ptr, uint32_t c_ptr); +void syscall_GlibGetOSVersionInfo(cpu_t* cpu, uint32_t a_ptr, uint32_t b_ptr, uint32_t c_ptr, uint32_t d_ptr); +void syscall_Malloc(cpu_t* cpu, uint32_t size, uint8_t clear_data); + +void syscall_Bfile_OpenFile_OS(cpu_t* cpu, const char* filename, int mode, int mode2); +void syscall_Bfile_CreateEntry_OS(cpu_t* cpu, const char* filename, int mode, int size_ptr); \ No newline at end of file diff --git a/fx9860-emulator/headers/main.h b/fx9860-emulator/headers/main.h index ca27b45..6a5740f 100644 --- a/fx9860-emulator/headers/main.h +++ b/fx9860-emulator/headers/main.h @@ -5,6 +5,12 @@ typedef struct memory_t memory_t; typedef struct cpu_t cpu_t; typedef struct display_t display_t; +#define USE_EMSCRIPT + +#ifdef USE_EMSCRIPT +#include +#endif + #include #include #include @@ -13,6 +19,7 @@ typedef struct display_t display_t; #include "display.h" #include "utils.h" #include "instructions/instructions.h" +#include "instructions/syscalls.h" // Initialization #define PC_PROGRAM_START 0x00300200 // Execution starting address @@ -21,9 +28,6 @@ typedef struct display_t display_t; #define SYSCALL_ADDRESS 0x80010070 // The calling convention to access the system calls, is to jump to 0x80010070 with the syscall number in r0 -// Status Register bits -#define SR_BIT_T 0 - // CPU struct cpu_t { // General registers R0 - R15 @@ -31,7 +35,12 @@ struct cpu_t { // Control registers uint32_t gbr; // Global base register - uint32_t sr; // Status register (0: T Bit) + uint32_t sr; // Status register + uint32_t ssr; // Saved status register + uint32_t spc; // Saved program counter + uint32_t vbr; // Vector base register + uint32_t sgr; // Saved general register + uint32_t dbr; // Debug base register // System registers uint32_t pc; // Program counter @@ -39,23 +48,24 @@ struct cpu_t { uint32_t mach; // Multiply-accumulate high uint32_t macl; // Multiply-accumulate low - // To sort - uint32_t vbr; // Vector base register - uint32_t ssr; // Saved status register - uint32_t sgr; // Saved general register - uint32_t spc; // Saved program counter - uint32_t dbr; // Debug base register - // Custom variables uint8_t isExecutionFinished; // Set to true when PR == PC_PROGRAM_START // Debug uint32_t instruction_count; - uint8_t* asciiTexture; + uint32_t instruction_per_frame; // External Components - memory_t* mem; // Memory + memory_t* mem; // Memory display_t* disp; // Display + + // (Temporary) Character sets + uint8_t* asciiTexture; + uint8_t* printMiniTexture; }; -void run_next_instruction(cpu_t* cpu); \ No newline at end of file +void run_next_instruction(cpu_t* cpu); + +void main_loop_html(void* arg); +void init_loop_html(cpu_t* cpu); +void init_loop_c(cpu_t* cpu); \ No newline at end of file diff --git a/fx9860-emulator/headers/memory.h b/fx9860-emulator/headers/memory.h index a11c04b..7aa574c 100644 --- a/fx9860-emulator/headers/memory.h +++ b/fx9860-emulator/headers/memory.h @@ -2,6 +2,7 @@ #include "main.h" +// Memory start addresses #define ROM_START 0x00300000 #define RAM_START_MMU_ALIAS 0x08100000 #define RAM_START 0x8801c000 // 0x88048000 on 35+E II @@ -9,11 +10,21 @@ #define YRAM_START 0xe5017000 #define ILRAM_START 0xe5200000 -#define ROM_SIZE (2048 * 1024) -#define RAM_SIZE (512 * 1024) -#define XRAM_SIZE (8 * 1024) -#define YRAM_SIZE (8 * 1024) -#define ILRAM_SIZE (4 * 1024) +// Memory sizes +#define ROM_SIZE (2048 * 1024) +#define RAM_SIZE (512 * 1024) +#define XRAM_SIZE (8 * 1024) +#define YRAM_SIZE (8 * 1024) +#define ILRAM_SIZE (4 * 1024) + +// Keyboard register addresses (SH3-specific) +#define KB_PORTB_CTRL 0xA4000102 +#define KB_PORTM_CTRL 0xA4000118 +#define KB_PORTA 0xA4000120 +#define KB_PORTB 0xA4000122 +#define KB_PORTM 0xA4000138 +#define KB_SIZE (0xA4000138 - 0xA4000102 + 1) // Max - min address + 1 (insclusive) +#define KB_START KB_PORTB_CTRL typedef struct{ uint32_t size; @@ -27,10 +38,13 @@ struct memory_t { uint8_t ram[RAM_SIZE]; // Random-access memory + uint8_t keyboard[10]; // Keyboard rows state + uint8_t keyboard_registers[KB_SIZE]; // Keyboard registers + + uint32_t* tmp; + // uint8_t xram[XRAM_SIZE]; - // uint8_t yram[YRAM_SIZE]; - // uint8_t ilram[ILRAM_SIZE]; // Malloc @@ -41,5 +55,7 @@ struct memory_t { uint8_t* get_memory_for_address(cpu_t* cpu, uint32_t address); uint32_t mem_read(cpu_t* cpu, uint32_t address, uint8_t bytes); +void mem_write(cpu_t* cpu, uint32_t address, uint32_t data, uint8_t bytes); -void mem_write(cpu_t* cpu, uint32_t address, uint32_t data, uint8_t bytes); \ No newline at end of file +uint32_t emulate_keyboard_register_read(cpu_t* cpu); +void emulate_lcd_register_write(cpu_t* cpu, uint32_t address, uint32_t data); \ No newline at end of file diff --git a/fx9860-emulator/headers/utils.h b/fx9860-emulator/headers/utils.h index c2dc952..d8ff343 100644 --- a/fx9860-emulator/headers/utils.h +++ b/fx9860-emulator/headers/utils.h @@ -10,6 +10,8 @@ void critical_error(const char* format, ...); // Prints the binary representation of a number void print_binary(uint32_t x, int n); -void loadAsciiTexture(cpu_t* cpu); +// Load a character set from an image +uint8_t* load_character_set(const char* path); -void cpu_debug(cpu_t* cpu); \ No newline at end of file +void cpu_debug(cpu_t* cpu); +void vram_debug(cpu_t* cpu); \ No newline at end of file diff --git a/fx9860-emulator/instructions/instructions.c b/fx9860-emulator/instructions/instructions.c index 21b79e0..beeb666 100644 --- a/fx9860-emulator/instructions/instructions.c +++ b/fx9860-emulator/instructions/instructions.c @@ -245,13 +245,12 @@ struct SR0 { #define PC cpu->pc #define PR cpu->pr #define R cpu->r -#define R0 cpu->r[0] #define MACH cpu->mach #define MACL cpu->macl -#define Read_Byte(addr) mem_read_nolog(cpu, addr, 1) -#define Read_Word(addr) mem_read_nolog(cpu, addr, 2) -#define Read_Long(addr) mem_read_nolog(cpu, addr, 4) +#define Read_Byte(addr) mem_read(cpu, addr, 1) +#define Read_Word(addr) mem_read(cpu, addr, 2) +#define Read_Long(addr) mem_read(cpu, addr, 4) #define Write_Byte(addr, data) mem_write(cpu, addr, data, 1) #define Write_Word(addr, data) mem_write(cpu, addr, data, 2) diff --git a/fx9860-emulator/instructions/syscalls.c b/fx9860-emulator/instructions/syscalls.c index fe6a330..64accfe 100644 --- a/fx9860-emulator/instructions/syscalls.c +++ b/fx9860-emulator/instructions/syscalls.c @@ -2,9 +2,9 @@ // Syscall entry point void run_syscall(cpu_t* cpu) { - uint32_t syscall_code = cpu->r[0]; + uint32_t syscall_code = R0; - printf("run_syscall: 0x%03X 0x%X 0x%X 0x%X 0x%X (Return: 0x%08X)\n", syscall_code, cpu->r[4], cpu->r[5], cpu->r[6], cpu->r[7], cpu->pr); + // printf("run_syscall 0x%03X, args: 0x%X 0x%X 0x%X 0x%X\n", syscall_code, cpu->r[4], cpu->r[5], cpu->r[6], cpu->r[7]); int arg1 = cpu->r[4]; int arg2 = cpu->r[5]; @@ -15,6 +15,7 @@ void run_syscall(cpu_t* cpu) { syscall_GetVRAMAddress(cpu); } else if (syscall_code == 0x144) { // Bdisp_AllClr_DDVRAM + printf("Run Syscall: Bdisp_AllClr_DDVRAM\n"); syscall_Bdisp_AllClr_DDVRAM(cpu); } else if (syscall_code == 0x807) { // locate @@ -23,60 +24,113 @@ void run_syscall(cpu_t* cpu) { else if (syscall_code == 0x808) { // Print uint8_t* mem = get_memory_for_address(cpu, arg1); const unsigned char* str = (const unsigned char*)mem; + // printf("Run Syscall: syscall_Print (%s)\n", str); syscall_Print(cpu, str); } - else if (syscall_code == 0x9AD) { // PrintXY + else if (syscall_code == 0x9AD) { // PrintXY uint8_t* mem = get_memory_for_address(cpu, arg3); const unsigned char* str = (const unsigned char*)mem; + // printf("Run Syscall: syscall_PrintXY (%d %d %s %d)\n", arg1, arg2, str, arg4); syscall_PrintXY(cpu, arg1, arg2, str, arg4); } - else if (syscall_code == 0xACD) { // malloc - syscall_Malloc(cpu, arg1); + else if (syscall_code == 0xC4F) { // PrintMiniSd + uint8_t* mem = get_memory_for_address(cpu, arg3); + const unsigned char* str = (const unsigned char*)mem; + // printf("Run Syscall: PrintMiniSd (%d %d %s %d)\n", arg1, arg2, str, arg4); + syscall_PrintMiniSd(cpu, arg1, arg2, str, arg4); } - else if (syscall_code == 0x462) { // GetAppName - syscall_GetAppName(cpu, arg1); + else if (syscall_code == 0xACD) { // malloc + syscall_Malloc(cpu, arg1, 0); + } + else if (syscall_code == 0xE6B) { // calloc + syscall_Malloc(cpu, arg1, 1); + } + else if (syscall_code == 0xE6D) { // realloc + printf("Skipped syscall: realloc\n"); + } + else if (syscall_code == 0xACC) { // free + printf("Skipped syscall: free\n"); + } + else if (syscall_code == 0x462) { // GetAppName + syscall_GetAppName(cpu, (char*)arg1); + } + else if (syscall_code == 0x014) { // GlibGetAddinLibInf + syscall_GlibGetAddinLibInf(cpu, arg1, arg2, arg3); + } + else if (syscall_code == 0x015) { // GlibGetOSVersionInfo + syscall_GlibGetOSVersionInfo(cpu, arg1, arg2, arg3, arg4); } else if (syscall_code == 0x90F) { // GetKey - printf("Syscall not implemented, skipping: GetKey\n"); - cpu->isExecutionFinished = 1; + printf("Skipped syscall: GetKey\n"); + // syscall_GetKey(cpu, arg1); + // printf("Run Syscall: GetKey (0x%08X: %d)\n", arg1, R0); + // cpu->isExecutionFinished = 1; } else if (syscall_code == 0x24C) { // Keyboard_IsSpecialKeyDown - printf("Syscall not implemented, skipping: Keyboard_IsSpecialKeyDown\n"); - cpu->isExecutionFinished = 1; + printf("Skipped syscall: Keyboard_IsSpecialKeyDown\n"); + } + else if (syscall_code == 0x03B) { // RTC_GetTicks + R0 = cpu->instruction_count / 7812; + // printf("Run Syscall: RTC_GetTicks %d\n", R0); } - else if (syscall_code == 0x43B) { // Bfile_FindFirst - printf("Syscall not implemented, skipping: Bfile_FindFirst\n"); - } else if (syscall_code == 0x434) { // Bfile_CreateEntry_OS - printf("Syscall not implemented, skipping: Bfile_CreateEntry_OS\n"); + uint8_t* mem = get_memory_for_address(cpu, arg1); + const char* str = (const char*)mem; + // syscall_Bfile_CreateEntry_OS(cpu, str, arg2, arg3); + printf("Skipped syscall: Bfile_CreateEntry_OS\n"); } else if (syscall_code == 0x42C) { // Bfile_OpenFile_OS - printf("Syscall not implemented, skipping: Bfile_OpenFile_OS\n"); - } + uint8_t* mem = get_memory_for_address(cpu, arg1); + const char* str = (const char*)mem; + // syscall_Bfile_OpenFile_OS(cpu, str, arg2, arg3); + printf("Skipped syscall: Bfile_OpenFile_OS\n"); + } + else if (syscall_code == 0x432) { // Bfile_ReadFile_OS + printf("Skipped syscall: Bfile_ReadFile_OS\n"); + } + else if (syscall_code == 0x435) { // Bfile_WriteFile_OS + printf("Skipped syscall: Bfile_WriteFile_OS\n"); + } + else if (syscall_code == 0x42D) { // Bfile_CloseFile_OS + printf("Skipped syscall: Bfile_CloseFile_OS\n"); + } + else if (syscall_code == 0x43B) { // Bfile_FindFirst + printf("Skipped syscall: Bfile_FindFirst\n"); + } + else if (syscall_code == 0x439) { // Bfile_DeleteEntry + printf("Skipped syscall: Bfile_DeleteEntry\n"); + } else if (syscall_code == 0x82B) { // MCSPutVar2 - printf("Syscall not implemented, skipping: MCSPutVar2\n"); + printf("Skipped syscall: MCSPutVar2\n"); } else if (syscall_code == 0x840) { // MCSGetDlen2 - printf("Syscall not implemented, skipping: MCSGetDlen2\n"); + printf("Skipped syscall: MCSGetDlen2\n"); } + else if (syscall_code == 0x420) { // OS_inner_Sleep // https://bible.planet-casio.com/simlo/chm/v20/fx_legacy_Sleep.HTM - printf("Syscall not implemented, skipping: OS_inner_Sleep\n"); - } - else if (syscall_code == 0x014) { // GlibGetAddinLibInf - printf("Syscall not implemented, skipping: GlibGetAddinLibInf\n"); - } - else if (syscall_code == 0x015) { // GlibGetOSVersionInfo - printf("Syscall not implemented, skipping: GlibGetOSVersionInfo\n"); + // printf("Skipped syscall: OS_inner_Sleep\n"); } else if (syscall_code == 0x494) { // void SetQuitHandler( void (*callback)(void) ); - printf("Syscall not implemented, skipping: SetQuitHandler\n"); + printf("Skipped syscall: SetQuitHandler\n"); + } + else if (syscall_code == 0x3ED) { // Interrupt_SetOrClrStatusFlags + // https://bible.planet-casio.com/simlo/chm/v20/fx_legacy_INTERRUPT.HTM + printf("Skipped syscall: Interrupt_SetOrClrStatusFlags\n"); + } + // Can be ignored + else if (syscall_code == 0x013) { // GlibAddinAplExecutionCheck + printf("Ignored syscall: GlibAddinAplExecutionCheck\n"); + } + else if (syscall_code == 0x3FA) { // Hmem_SetMMU + printf("Ignored syscall: Hmem_SetMMU\n"); } - // Can be skipped - else if (syscall_code == 0x013) {} // GlibAddinAplExecutionCheck - else if (syscall_code == 0x3FA) {} // Hmem_SetMMU + // else if (syscall_code == 0x1032) { // return a pointer to a matrixcode/keycode mapping table + // // https://bible.planet-casio.com/simlo/chm/v20/fx_legacy_keyboard.htm + // printf("Skipped syscall: 0x1032\n"); + // } else { printf("Syscall not implemented: 0x%03X\n", syscall_code); cpu->isExecutionFinished = 1; @@ -107,15 +161,63 @@ void syscall_Bdisp_AllClr_DDVRAM(cpu_t* cpu) { void syscall_Locate(cpu_t* cpu, int x, int y) { cpu->disp->cursor.col = x; cpu->disp->cursor.row = y; - printf("syscall_locate: %d %d\n", x, y); + printf("Run Syscall: locate (%d %d)\n", x, y); +} + +void draw_character_mini(cpu_t* cpu, const char c, int pixel_start_x, int pixel_start_y, int mode) { + // Character x,y in ASCII texture (16x16) + int char_x = c / 16; + int char_y = c % 16; + + if (char_x < 0 || char_x >= 16 || char_y < 0) return; + + // Pixel start x, y in ASCII texture (128x128) + char_x = char_x * 7; + char_y = char_y * 7; + + for (int y = 0; y < 7; y++) { + for (int x = 0; x < 7; x++) { + // Current pixel position in ASCII texture + int ascii_x = char_x + x; + int ascii_y = char_y + y; + int ascii_id = ascii_x + ascii_y * 128; // Pixel index + if (ascii_id < 0 || ascii_id >= 128 * 128) break; + + // Current pixel position on-screen + int vram_x = pixel_start_x + x; + int vram_y = pixel_start_y + y; + int vram_id = (vram_x + vram_y * SCREEN_WIDTH) / 8; // index in VRAM + if (vram_id < 0 || vram_id >= VRAM_SIZE) break; + + int ascii_bit = cpu->printMiniTexture[ascii_id] ^ mode; // Pixel value + int vram_bit = 7 - vram_x % 8; // Current bit + + // Change one single bit in the VRAM + if (ascii_bit) + cpu->disp->vram[vram_id] = (cpu->disp->vram[vram_id] & ~(1 << vram_bit)) | (1 << vram_bit); + } + } +} + +void syscall_PrintMiniSd(cpu_t* cpu, int x, int y, const unsigned char* str, int mode) { + int i = 0; + while (x < SCREEN_WIDTH) { + const char c = str[i++]; // Current character + + if (c == 0x00) break; // Line terminator + + draw_character_mini(cpu, c, x, y, mode); + + x += 4; // Move the cursor the the right + } } // Draw a character on screen // pixel_start_x and y represent the top left pixel of the character -void draw_character(cpu_t* cpu, const char c, int pixel_start_x, int pixel_start_y) { +void draw_character(cpu_t* cpu, const char c, int pixel_start_x, int pixel_start_y, int mode) { int offset = c - 0x0020; - // Character x,y in ASCII texture (16x6) + // Character x,y in ASCII texture (16x16) int char_x = offset % 16; int char_y = offset / 16; // Pixel start x, y in ASCII texture (112x54) @@ -128,9 +230,8 @@ void draw_character(cpu_t* cpu, const char c, int pixel_start_x, int pixel_start int ascii_x = char_x + x; int ascii_y = char_y + y; int ascii_id = ascii_x + ascii_y * 112; // Pixel index - int ascii_bit = cpu->asciiTexture[ascii_id]; // Pixel value - if (ascii_id < 0 || ascii_id > 112 * 54) critical_error("ASCII Texture Access out of bounds: %d %d (%d)", ascii_x, ascii_y, ascii_id); + if (ascii_id < 0 || ascii_id >= 112 * 54) break; // critical_error("ASCII Texture Access out of bounds: %d %d (%d)", ascii_x, ascii_y, ascii_id); // Current pixel position on-screen int vram_x = pixel_start_x + x; @@ -138,10 +239,13 @@ void draw_character(cpu_t* cpu, const char c, int pixel_start_x, int pixel_start int vram_id = (vram_x + vram_y * SCREEN_WIDTH) / 8; // index in VRAM int vram_bit = 7 - vram_x % 8; // Current bit - if (vram_id < 0 || vram_id > VRAM_SIZE) critical_error("VRAM Access out of bounds: %d %d (%d)", vram_x, vram_y, vram_id); + if (vram_id < 0 || vram_id >= VRAM_SIZE) break; // critical_error("VRAM Access out of bounds: %d %d (%d)", vram_x, vram_y, vram_id); // Change one single bit in the VRAM - cpu->disp->vram[vram_id] = (cpu->disp->vram[vram_id] & ~(1 << vram_bit)) | (ascii_bit << vram_bit); + int ascii_bit = cpu->asciiTexture[ascii_id] ^ mode; // Pixel value + + if (ascii_bit) + cpu->disp->vram[vram_id] = (cpu->disp->vram[vram_id] & ~(1 << vram_bit)) | (ascii_bit << vram_bit); } } } @@ -163,34 +267,32 @@ void syscall_Print(cpu_t* cpu, const unsigned char* str) { screen_x = screen_x * CHAR_WIDTH + 1; screen_y = screen_y * CHAR_HEIGHT; - draw_character(cpu, c, screen_x, screen_y); + draw_character(cpu, c, screen_x, screen_y, 0); // Move the cursor the the right cpu->disp->cursor.col++; } } -void syscall_PrintXY(cpu_t* cpu, int x, int y, const unsigned char* str, int type) { - printf("syscall_PrintXY: %d %d %s\n", x, y, str); - +void syscall_PrintXY(cpu_t* cpu, int x, int y, const unsigned char* str, int mode) { int i = 0; while (x < SCREEN_WIDTH) { const char c = str[i++]; // Current character if (c == 0x00) break; // Line terminator - draw_character(cpu, c, x, y); + draw_character(cpu, c, x, y, mode); x += CHAR_WIDTH; // Move the cursor the the right } } -#define MALLOC_MEM_LOW RAM_START_MMU_ALIAS + (32 * 1042) +#define MALLOC_MEM_LOW RAM_START_MMU_ALIAS + (32 * 1024) #define MALLOC_MEM_HIGH RAM_START_MMU_ALIAS + RAM_SIZE - (16 * 1024) #define MALLOC_MARGIN 0x200 // Allocate memory and return the address of the allocated memory in r0 -void syscall_Malloc(cpu_t* cpu, uint32_t size) { +void syscall_Malloc(cpu_t* cpu, uint32_t size, uint8_t clear_data) { memory_t* mem = cpu->mem; // Initialize with the lowest address @@ -206,35 +308,146 @@ void syscall_Malloc(cpu_t* cpu, uint32_t size) { mem->mallocCount++; mem->mallocs = realloc(mem->mallocs, mem->mallocCount * sizeof(malloc_info_t)); + if (clear_data) { + for (int i = 0; i < size; i++) { + mem_write(cpu, addr + i, 0, 4); + } + } + if (mem->mallocs == 0) { critical_error("syscall_malloc(): Could not allocate memory"); } + if (clear_data) + printf("Run Syscall: Calloc #%d (size: %d)\n", mem->mallocCount - 1, size); + else + printf("Run Syscall: Malloc #%d (size: %d)\n", mem->mallocCount - 1, size); + mem->mallocs[mem->mallocCount - 1] = (malloc_info_t){size, addr}; - printf("syscall_malloc %d : 0x%08X\n", mem->mallocCount, addr); - // Return the address of the allocated memory - cpu->r[0] = addr; + R0 = addr; } // https://bible.planet-casio.com/simlo/chm/v20/fx_legacy_AppName.HTM // Copies the registered name for the running application into the character array dest. // dest must be able to hold 9 bytes. dest is returned. void syscall_GetAppName(cpu_t* cpu, char* dest) { - // The app names is stored in 9 bytes - dest = malloc(9 * sizeof(char)); - // Copies the app name to the new buffer - memcpy(dest, &cpu->mem->rom[0x20], 9); + for (int i = 0; i < 9; i++) { + mem_write(cpu, (uint32_t)dest + i, cpu->mem->rom[0x20 + i], 1); + } - // Return the buffer - cpu->r[0] = dest; + printf("Run Syscall: GetAppName (%s)\n", (const char*)get_memory_for_address(cpu, (uint32_t)dest)); - printf("syscall_GetAppName %s\n", dest); + R0 = (int32_t)dest; // Return the buffer } void syscall_GetVRAMAddress(cpu_t* cpu) { - cpu->r[0] = VRAM_ADDRESS; // &cpu->mem->ram[VRAM_ADDRESS]; - //printf("syscall_GetVRAMAddress 0x%08X\n", cpu->r[0]); -} \ No newline at end of file + R0 = VRAM_ADDRESS; + // printf("Run Syscall: GetVRAMAddress (-> 0x%08X)\n", R0); +} + +void syscall_GlibGetAddinLibInf(cpu_t* cpu, uint32_t a_ptr, uint32_t b_ptr, uint32_t c_ptr) { + // Mimic Casio SDK Emulator + mem_write(cpu, a_ptr, 0x0, 4); + mem_write(cpu, b_ptr, 0x1, 4); + mem_write(cpu, c_ptr, 0x1, 4); + + cpu->r[0] = 0x1;// 0x0; + cpu->r[2] = 0xA0151F28; + cpu->r[3] = 0x0; + cpu->r[4] = 0x1; + + printf("Run Syscall: GlibGetAddinLibInf\n"); +} + +void syscall_GlibGetOSVersionInfo(cpu_t* cpu, uint32_t a_ptr, uint32_t b_ptr, uint32_t c_ptr, uint32_t d_ptr) { + // Mimic Casio SDK Emulator + mem_write(cpu, a_ptr, 0x1, 1); + mem_write(cpu, b_ptr, 0x3, 1); + mem_write(cpu, c_ptr, 0x0, 2); + mem_write(cpu, d_ptr, 0x0, 2); + + cpu->r[0] = 0x1; + cpu->r[2] = 0x3; + cpu->r[3] = 0x1; + cpu->r[4] = 0x0; + + // printf("Run Syscall: GlibGetOSVersionInfo\n"); +} + +const char* convertFileName(const char* filename) { + char name[40]; + char ch; + int i; + + // Convert filename from { 0x00, 0xXX, 0x00, 0xXX, ... } to { 0xXX, 0xXX, ... } + for (i = 0; i < 40; i++) { + uint16_t ch = *(uint16_t*) &filename[i * 2 + 1]; + name[i] = ch; + if (ch == 0x00) break; + } + + // Remove '\\fls0\' + i -= 6; + char* final_name = malloc(sizeof(char) * i); + memcpy(final_name, name + 7, i); + + return (const char*)final_name; +} + +// mode: 1 = read, 2 = write +void syscall_Bfile_OpenFile_OS(cpu_t* cpu, const char* filename, int mode, int mode2) { + const char* name = convertFileName(filename); + + FILE* handle = fopen(name, mode == 1 ? "rb" : "wb"); + + printf("Run Syscall: Bfile_OpenFile_OS (%s, 0x%X, 0x%X) handle: 0x%X\n", name, mode, mode2, (uint32_t)handle); + + R0 = handle == NULL ? -1 : (int32_t)handle; +} + +void syscall_Bfile_CreateEntry_OS(cpu_t* cpu, const char* filename, int mode, int size_ptr) { + const char* name = convertFileName(filename); + + FILE* handle; + + if (mode == 0x1) { // Create file + handle = fopen(name, "wb"); + fclose(handle); + } + // else if (mode == 0x5) { } // Create folder + else printf("[Warning] CreateEntry mode not implemented\n"); + + printf("Run Syscall: Bfile_CreateEntry_OS (%s, 0x%X) size: %d, handle: 0x%X\n", name, mode, mem_read(cpu, size_ptr, 4), (uint32_t)handle); + + R0 = handle == NULL ? -1 : (int32_t)handle; +} + +// #ifdef USE_EMSCRIPT +// EM_ASYNC_JS(int, async_browser_getkey, (), { +// return await new Promise((resolve, reject) => { +// const onClick = (event) => { +// const res = window.keyboardEvent(event); +// if (res != null) { +// document.getElementById("keyboard").removeEventListener("click", onClick); +// resolve(res); +// } +// }; +// document.getElementById("keyboard").addEventListener("click", onClick); +// }); +// }) +// #else +// int async_browser_getkey() { +// return 0; +// } +// #endif + +// void syscall_GetKey(cpu_t* cpu, unsigned int keycode_address) { +// int key = async_browser_getkey(); + +// mem_write(cpu, keycode_address, key, 4); + +// R0 = key; +// } \ No newline at end of file diff --git a/fx9860-emulator/main.c b/fx9860-emulator/main.c index 0e6e379..92b9689 100644 --- a/fx9860-emulator/main.c +++ b/fx9860-emulator/main.c @@ -38,6 +38,13 @@ memory_t* init_memory(uint8_t* g1a_content, uint32_t g1a_size) { mem->rom = g1a_content; mem->rom_size = g1a_size; + + mem->mallocs = calloc(1, sizeof(uint8_t)); + mem->tmp = calloc(1024, sizeof(uint8_t)); + + // Initialize keyboard rows (1 = key not pressed) + for (int i = 0; i < 10; i++) + mem->keyboard[i] = 0xFF; return mem; } @@ -48,7 +55,6 @@ display_t* init_display(memory_t* memory) { if(disp == NULL) critical_error("Could not allocate memory for Display"); - //disp->vram = &memory->ram[VRAM_ADDRESS]; disp->cursor.row = 1; disp->cursor.col = 1; @@ -95,10 +101,7 @@ void run_next_instruction(cpu_t* cpu) { } // Extract 16bits instruction code - uint16_t instruction = (mem_read_nolog(cpu, address, 1) << 8) | mem_read_nolog(cpu, address + 1, 1); - - if (cpu->instruction_count % 1000000 == 0) - cpu_debug(cpu); + uint16_t instruction = (mem_read(cpu, address, 1) << 8) | mem_read(cpu, address + 1, 1); // Get a pointer to the function implementing the current instruction get_instruction_impl(instruction) @@ -108,44 +111,84 @@ void run_next_instruction(cpu_t* cpu) { int main() { // Load G1A uint32_t g1a_size; - uint8_t* g1a_content = load_g1a("SAMPLE_ADDIN.G1A", &g1a_size); + uint8_t* g1a_content = load_g1a("JJSH3.G1A", &g1a_size); // Init CPU, Memory and Display memory_t* mem = init_memory(g1a_content, g1a_size); display_t* disp = init_display(mem); cpu_t* cpu = init_cpu(mem, disp); - loadAsciiTexture(cpu); // (Temporary) load the ASCII font texture + // (Temporary) load the ASCII font texture + cpu->asciiTexture = load_character_set("U+0020.png"); + cpu->printMiniTexture = load_character_set("PrintMini.bmp"); +#ifdef USE_EMSCRIPT + init_loop_html(cpu); +#else + init_loop_c(cpu); +#endif - /// [Main Loop] - /// Execute add-in (up to 20M instructions for now) - for (int i = 0; i < 2e7; i++) { + return 0; +} + +// Init the HTML version of the emulator +void init_loop_html(cpu_t* cpu) { +#ifdef USE_EMSCRIPT + cpu->instruction_per_frame = 10000; + + // Pass useful pointers to javascript + EM_ASM({ + window.lcdImage = $0; + window.keyboardPointer = $1; + window.instructionPerFrame = $2; + window.instructionCount = $3; + window.cpuPointer = $4; + window.initGUI(); + }, + &cpu->disp->vram, + &cpu->mem->keyboard, + &cpu->instruction_per_frame, + &cpu->instruction_count, + cpu + ); + + // Setup the main animation frame loop + emscripten_set_main_loop_arg(main_loop_html, cpu, -1, 0); +#endif +} + +// Main loop for the HTML version of the emulator +void main_loop_html(void* arg) { + cpu_t* cpu = (cpu_t*)arg; + + if (cpu->isExecutionFinished) return; + + for (int i = 0; i < cpu->instruction_per_frame; i++) { + run_next_instruction(cpu); + cpu->instruction_count++; + + if (cpu->isExecutionFinished) break; + } +} + +// Run the console version of the emulator +// (no display - VRAM dump at the end) +void init_loop_c(cpu_t* cpu) { + // Run up to 10M instructions + for (int i = 0; i < 1e7; i++) { run_next_instruction(cpu); cpu->instruction_count++; if (cpu->isExecutionFinished) break; } + vram_debug(cpu); - // (Temporary) Display VRAM - for (int y = 0; y < 64; y++) { - for (int x = 0; x < 16; x++) { - for (int b = 7; b >= 0; b--) { - int id = x + y * 16; - if (id < 0 || id >= VRAM_SIZE) critical_error("error %d", id); - int bit = (disp->vram[id] >> b) & 1; - printf(bit || y == 0 || y == 63 || (x==0 && b==7) || (x==15 && b==0) ? "X" : " "); - } - } - printf("\n"); - } - - printf("\n"); - - free(g1a_content); - free(mem); + free(cpu->mem->mallocs); + free(cpu->mem->tmp); + free(cpu->mem->rom); + free(cpu->mem); + free(cpu->asciiTexture); + free(cpu->printMiniTexture); free(cpu); - - return 0; } \ No newline at end of file diff --git a/fx9860-emulator/memory.c b/fx9860-emulator/memory.c index 7d7bce5..23a4a41 100644 --- a/fx9860-emulator/memory.c +++ b/fx9860-emulator/memory.c @@ -3,36 +3,42 @@ // Returns a pointer to the buffer (memory) // corresponding to the `address` parameter uint8_t* get_memory_for_address(cpu_t* cpu, uint32_t address) { - if (address >= ROM_START && address < ROM_START + cpu->mem->rom_size) { - // (ROM) - MMU + if (address >= ROM_START && address < ROM_START + cpu->mem->rom_size) { // ROM - (MMU) return &cpu->mem->rom[address - ROM_START]; } - else if (address >= RAM_START_MMU_ALIAS && address < RAM_START_MMU_ALIAS + RAM_SIZE) { - // (RAM) - MMU + else if (address >= RAM_START_MMU_ALIAS && address < RAM_START_MMU_ALIAS + RAM_SIZE) { // RAM - (MMU) return &cpu->mem->ram[address - RAM_START_MMU_ALIAS]; } - else if (address >= RAM_START && address < RAM_START + RAM_SIZE) { - // (RAM) - Physical + else if (address >= RAM_START && address < RAM_START + RAM_SIZE) { // RAM - (Physical) return &cpu->mem->ram[address - RAM_START]; } - else if (address >= VRAM_ADDRESS && address < VRAM_ADDRESS + VRAM_SIZE) { - // (VRAM) + else if (address >= VRAM_ADDRESS && address < VRAM_ADDRESS + VRAM_SIZE) { // VRAM return &cpu->disp->vram[address - VRAM_ADDRESS]; } - else if (address == LCD_SELECT_REGISTER || address == LCD_DATA_REGISTER) { - // LCD emulation + else if (address == LCD_SELECT_REGISTER || address == LCD_DATA_REGISTER) { // LCD registers return &cpu->disp->lcd_registers; } + else if (address >= KB_START && address < KB_START + KB_SIZE) { // Keyboard registers + return &cpu->mem->keyboard_registers[address - KB_START]; + } + else if (address >= 0xFFFFFEE0 && address < 0xFFFFFFFF) { // Temporary memory + return &cpu->mem->tmp[address - 0xFFFFFEE0]; + } else { + // cpu_debug(cpu); critical_error("Request for memory at address 0x%08X is out of bounds\n", address); } } // Write to the memory void mem_write(cpu_t* cpu, uint32_t address, uint32_t data, uint8_t bytes) { - uint8_t* mem = get_memory_for_address(cpu, address); + // LCD input registers + if (address == LCD_SELECT_REGISTER || address == LCD_DATA_REGISTER) { + emulate_lcd_register_write(cpu, address, data); + return; + } - //printf("Memory write [0x%08X]: 0x%08X\n", address, data); + uint8_t* mem = get_memory_for_address(cpu, address); if (bytes == 1) { *mem = data & 0xFF; @@ -50,24 +56,99 @@ void mem_write(cpu_t* cpu, uint32_t address, uint32_t data, uint8_t bytes) { } // Read from the memory -uint32_t mem_read_(cpu_t* cpu, uint32_t address, uint8_t bytes) { +uint32_t mem_read(cpu_t* cpu, uint32_t address, uint8_t bytes) { + // Keyboard output register + if (address == KB_PORTA) { + return emulate_keyboard_register_read(cpu); + } + // Address that stores the model of the calculator (0 for emulator) + else if (address == 0x80000300) { + return 0; + } + uint8_t* mem = get_memory_for_address(cpu, address); + uint32_t data; if (bytes == 1) { - return *mem; + data = *mem; } else if (bytes == 2) { - return (*mem << 8) | (*(mem + 1)); + data = (*mem << 8) | (*(mem + 1)); } else { - return (*mem << 24) | (*(mem + 1) << 16) | (*(mem + 2) << 8) | (*(mem + 3)); + data = (*mem << 24) | (*(mem + 1) << 16) | (*(mem + 2) << 8) | (*(mem + 3)); + } + + // printf("Memory read [0x%08X]: : 0x%X\n", address, data); + + return data; +} + +// Emulate keyboard register A output by reading values +// from port B and M control registers. +// Returns the state of the keyboard for the current row +// https://www.planet-casio.com/Fr/forums/topic17509-2-emulateur-fx-9860-sh4.html#194390 +uint32_t emulate_keyboard_register_read(cpu_t* cpu) { + uint16_t PORTB_CTRL = mem_read(cpu, KB_PORTB_CTRL, 2); + uint8_t row = 0; + + if (PORTB_CTRL != 0xAAAA) { // Rows 0-7 + uint16_t smask = PORTB_CTRL ^ 0xAAAA; + + while (smask >> (row*2) > 0b11) row++; + } + else { // Rows 8-9 + uint16_t PORTM_CTRL = mem_read(cpu, KB_PORTM_CTRL, 2); // row 8: xxxx0001 row 9: xxx0010 + + row = 8 + ((PORTM_CTRL & 0b11) >> 1); + } + + if (row > 9) { + printf("[Warning] Request for keyboard row %d which does not exist!\n", row); + return 0xffffffff; + } + else { + return cpu->mem->keyboard[row]; } } -uint32_t mem_read_nolog(cpu_t* cpu, uint32_t address, uint8_t bytes) { - return mem_read_(cpu, address, bytes); -} -uint32_t mem_read(cpu_t* cpu, uint32_t address, uint8_t bytes) { - uint32_t value = mem_read_(cpu, address, bytes); - printf("Memory read [0x%08X]: : 0x%X\n", address, value); - return value; + +// Keep track of current row and column set in the LCD selector register +// and write data to the LCD buffer. +// Refresh the HTML canvas when the last row is written. +// Works well for MonochromeLib's DisplayVRAM() function, but might +// not work for more specific register accesses. +void emulate_lcd_register_write(cpu_t* cpu, uint32_t address, uint32_t data) { + static int row = -1; + static int col = 0; + + // Setup LCD row + if (address == LCD_SELECT_REGISTER) { + if (data == 7) { // New row + row++; + + if (row >= 64) { + row = 0; + col = 0; + } + } + // printf("LCD REGISTER Write: 0x%08X, set row: %d\n", data, row); + } + // Write data to LCD row + else if (address == LCD_DATA_REGISTER) { + if (row >= 0) { + cpu->disp->lcd[row * 16 + col++] = data; // Update LCD buffer + +#ifdef EMSCRIPTEN + if (row == 63 && col == 16) { // Refresh HTML canvas + EM_ASM({ + window.refreshScreen(); + }); + } +#endif + if (col >= 16) { + col = 0; + } + } + // printf("LCD DATA Write: 0x%08X, row: %d, col: %d\n", data, row, col); + } } \ No newline at end of file diff --git a/fx9860-emulator/utils.c b/fx9860-emulator/utils.c index ae938e2..bbfbf96 100644 --- a/fx9860-emulator/utils.c +++ b/fx9860-emulator/utils.c @@ -8,8 +8,14 @@ void critical_error(const char* format, ...) { va_list args; va_start(args, format); + printf("[Error] "); vprintf(format, args); va_end(args); + +#ifdef USE_EMSCRIPT + emscripten_cancel_main_loop(); +#endif + exit(EXIT_FAILURE); } @@ -21,65 +27,79 @@ void print_binary(uint32_t x, int n) { } } -void loadAsciiTexture(cpu_t* cpu) { - const char* file_path = "U+0020.png"; - +// Load a character set from an image +uint8_t* load_character_set(const char* path) { // Load the PNG image using stb_image int width, height, channels; - unsigned char* image_data = stbi_load(file_path, &width, &height, &channels, STBI_rgb); + unsigned char* image_data = stbi_load(path, &width, &height, &channels, STBI_rgb); - // Check if the image loading was successful if (image_data == NULL) { - printf("Error loading image: %s\n", stbi_failure_reason()); - return -1; + critical_error("Could not load character set %s: %s\n", path, stbi_failure_reason()); } - // Check if the image dimensions match the requirements - /*if (width != 112 || height != 54 || channels != 3) { - printf("Image dimensions or bit depth do not match requirements.\n"); - stbi_image_free(image_data); - return -1; - }*/ - - uint8_t* asciiTexture = malloc(width * height * sizeof(uint8_t)); + uint8_t* packed_data = malloc(width * height * sizeof(uint8_t)); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = (y * width + x); int indexRGB = (y * width + x) * channels; unsigned char red = image_data[indexRGB]; - asciiTexture[index] = red ? 0 : 1; - //printf(asciiTexture[index] ? "X" : " "); + if (index >= width * height) critical_error("error %d", index); + packed_data[index] = red ? 0 : 1; + //printf(packed_data[index] ? "X" : " "); } //printf("\n"); } - cpu->asciiTexture = asciiTexture; - // Free the allocated memory for the image data stbi_image_free(image_data); + + printf("Loaded character set %s: %dx%dx%d\n", path, width, height, channels); + + return packed_data; } +// Prints the current state of the CPU void cpu_debug(cpu_t* cpu) { - uint16_t instruction = (mem_read_nolog(cpu, cpu->pc, 1) << 8) | mem_read_nolog(cpu, cpu->pc + 1, 1); - uint8_t high = (instruction >> 12) & 0x0F; - uint8_t high8 = instruction >> 8; - uint8_t low = instruction & 0x0F; - uint8_t low8 = instruction & 0xFF; + if (cpu->pc != 0x80010070) { + uint16_t instruction = (mem_read(cpu, cpu->pc, 1) << 8) | mem_read(cpu, cpu->pc + 1, 1); + uint8_t high = (instruction >> 12) & 0x0F; + uint8_t high8 = instruction >> 8; + uint8_t low = instruction & 0x0F; + uint8_t low8 = instruction & 0xFF; - printf("[%d][0x%08X] instruction: 0x%04X -", cpu->instruction_count, cpu->pc, instruction); - print_binary(instruction, 16); - print_binary(high, 4); - print_binary(low, 4); - printf("\n"); - - /* - for (int i = 0; i < 16; i++) { - int r = cpu->r[i]; - if (r != 0) - printf("r%d: 0x%08X, ", i, r); + printf("[%d][0x%08X] instruction: 0x%04X -", cpu->instruction_count, cpu->pc, instruction); + print_binary(instruction, 16); + print_binary(high, 4); + print_binary(low, 4); + printf("\n"); } - printf("pr: 0x%08X, cursor: %d-%d, T: %d\n", cpu->pr, cpu->disp->cursor.col, cpu->disp->cursor.row, get_status_register_bit(cpu, SR_BIT_T)); - */ - //printf("mem: 0x%02X 0x%02X 0x%02X 0x%02X\n", mem_read_nolog(cpu, 0x00300670, 1), mem_read_nolog(cpu, 0x00300670 + 1, 1), mem_read_nolog(cpu, 0x00300670 + 2, 1), mem_read_nolog(cpu, 0x00300670 + 3, 1)); + else printf("[%d][0x%08X] instruction: syscall\n", cpu->instruction_count, cpu->pc); + + for (int i = 0; i < 16; i++) { + int r = cpu->r[i]; + if (r != 0) + printf("r%d: 0x%08X, ", i, r); + } + + printf("pr: 0x%08X, cursor: %d-%d, T: %d\n", cpu->pr, cpu->disp->cursor.col, cpu->disp->cursor.row, (cpu->sr >> 31) & 1); + // printf("pr: 0x%08X, macl: 0x%08X, gbr: 0x%08X, T: %d\n", cpu->pr, cpu->macl, cpu->gbr, cpu->disp->cursor.col, cpu->disp->cursor.row, (cpu->sr >> 31) & 1); + // printf("mem: 0x%02X 0x%02X 0x%02X 0x%02X\n", mem_read(cpu, 0x00300670, 1), mem_read(cpu, 0x00300670 + 1, 1), mem_read(cpu, 0x00300670 + 2, 1), mem_read(cpu, 0x00300670 + 3, 1)); +} + +// Prints the current state of the VRAM +void vram_debug(cpu_t* cpu) { + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 16; x++) { + for (int b = 7; b >= 0; b--) { + int id = x + y * 16; + if (id < 0 || id >= VRAM_SIZE) critical_error("error %d", id); + int bit = ((cpu->disp->vram[id] >> b) & 1) || y == 0 || y == 63 || (x == 0 && b == 7) || (x == 15 && b == 0); + + printf(bit ? "_" : "X"); + } + } + printf("\n"); + } + printf("\n"); } \ No newline at end of file diff --git a/out/build/x64-debug/fx9860-emulator/SAMPLE_ADDIN.G1A b/out/build/x64-debug/fx9860-emulator/SAMPLE_ADDIN.G1A deleted file mode 100644 index af65d60aa78fecc257de18ef82d4fc1a84a7a0ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1684 zcmeH`Pi)&%9LIkzb`z(LM6(g81EIuDNnq7tC+SLHYS%bvwQi#kbvG)cq9k^pY10sz zXk5lRqyde*s*^f#KpY}X;(#h~K;i`A(20S>0pd~)R4nvQHY7G!5D}{?!q07ymP(vB zaN*e>|K8U>zvuV+z2Ey_@zW2l%)UEkYy5)O;XfOV`;#BRLLBy5-$=ha^0z=M`hB8^ zwjE2lVmh zEA-=MxNYxEZkwZbId1uJ8-Q%jG#Y6CZ~rC(g;c)q&*RNkAN1)Y(g{|4Bbw?lUS}gL zSj(-Ow8UO6>aJBa2{?Ej251L)U-z`7-&Dy#^f z$M~|C<$bq{WC%S!_=eaGGh#s)#EP`8K6#hE1E*P4f1UKKYg?8Rx_LM9k;@lOzL#UaDi5mT>))t#i={hK-pQMQoM4uBT%4|$u3^IuS_SB z{*eC2tF*Cp_kp+^<7fU5q|a8~s8vbXky0=|SbkV{81cPXOHpbqHKre!VIrL|57Pz= z#+N}ZBcCAe&5{Z*Z7w4Rs~9)t!F&;!MJ^$iag5>n3iJ8`+C{XsGWpshb%OOGfgINQ zp?x~KPv2_RrhR_;>OOyavxbAsn%ZjC4AXDd=(S@%A!MT>^+S9vpElg1AdV%1q3&~nyQKhDdz^kX_-!2g4ShPpJ*Si$%>3DSh_TFDA zGi@=?cs*U8s88078U{972XHZhr+7-}UIWe>e8zv+Z3%Y->HikLFMd$L^(E`yP-UtD z9B=f&igio_&_m6&1DhyAruvCaok2=~uF@bRJy!#8fVma)nj($Y9oo S=LcxxZ0%vgy}N>^2L1wXxO4&l