From 43e9e8226445847c8813c94adfb689b5bd037fb8 Mon Sep 17 00:00:00 2001 From: OmniaX Date: Wed, 27 Dec 2023 17:21:07 +0100 Subject: [PATCH] First Commit --- .gitignore | 4 + .vscode/c_cpp_properties.json | 31 + .vscode/settings.json | 143 +++ CMakeLists.txt | 181 +++ README.md | 22 + compile | 16 + extra/config/testMachine.dvm | 3 + extra/dragon info/DragonVM Memory Structure | 79 ++ extra/dragon info/Interrupts | 70 ++ extra/dragon info/Memory Map.png | Bin 0 -> 124438 bytes extra/dragon info/regular_expressions.txt | 10 + extra/dragon/bios.bin | Bin 0 -> 1024 bytes extra/dragon/cmos.dr | Bin 0 -> 128 bytes extra/dragon/disk1.dr | Bin 0 -> 4194304 bytes extra/dss/bios.dss | 140 +++ extra/dss/handlerTest.dss | 94 ++ extra/dss/mbr.dss | 7 + extra/dss/newTest.dss | 15 + extra/dss/test.dss | 8 + extra/font.bmp | Bin 0 -> 109962 bytes extra/make_and_run | 25 + extra/scripts/bios_flash | 10 + extra/scripts/load_mbr | 13 + src/assembler/Assembler.cpp | 1152 +++++++++++++++++++ src/assembler/Assembler.hpp | 85 ++ src/assembler/assembler_main.cpp | 60 + src/debugger/Debugger.cpp | 829 +++++++++++++ src/debugger/Debugger.hpp | 72 ++ src/debugger/DisassemblyLoader.cpp | 137 +++ src/debugger/DisassemblyLoader.hpp | 49 + src/debugger/debugger_main.cpp | 100 ++ src/gui/DebuggerWindow.cpp | 6 + src/gui/DebuggerWindow.hpp | 9 + src/gui/RawTextRenderer.cpp | 101 ++ src/gui/RawTextRenderer.hpp | 30 + src/gui/VirtualConsole.cpp | 158 +++ src/gui/VirtualConsole.hpp | 64 ++ src/gui/VirtualConsoleOutputHandler.cpp | 303 +++++ src/gui/VirtualConsoleOutputHandler.hpp | 99 ++ src/gui/Window.cpp | 98 ++ src/gui/Window.hpp | 57 + src/gui/widgets/VirtualConsoleWidget.cpp | 128 +++ src/gui/widgets/VirtualConsoleWidget.hpp | 67 ++ src/gui/widgets/Widget.hpp | 25 + src/hardware/IMemoryDevice.hpp | 19 + src/hardware/MemoryMapper.cpp | 75 ++ src/hardware/MemoryMapper.hpp | 41 + src/hardware/VirtualCPU.cpp | 707 ++++++++++++ src/hardware/VirtualCPU.hpp | 54 + src/hardware/VirtualHardDrive.cpp | 106 ++ src/hardware/VirtualHardDrive.hpp | 38 + src/hardware/VirtualIODevices.cpp | 655 +++++++++++ src/hardware/VirtualIODevices.hpp | 237 ++++ src/hardware/VirtualRAM.cpp | 45 + src/hardware/VirtualRAM.hpp | 25 + src/runtime/ConfigLoader.cpp | 53 + src/runtime/ConfigLoader.hpp | 30 + src/runtime/DragonRuntime.cpp | 382 ++++++ src/runtime/DragonRuntime.hpp | 94 ++ src/runtime/runtime_main.cpp | 74 ++ src/tools/GlobalData.hpp | 424 +++++++ src/tools/SDLInclude.hpp | 7 + src/tools/Utils.cpp | 7 + src/tools/Utils.hpp | 11 + src/tools/tools_main.cpp | 122 ++ tools/CppResourceMaker | Bin 0 -> 45144 bytes tools/build.nr | 1 + tools/inc_bnr | Bin 0 -> 16880 bytes tools/inc_bnr.exe | Bin 0 -> 136717 bytes 69 files changed, 7707 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 compile create mode 100644 extra/config/testMachine.dvm create mode 100644 extra/dragon info/DragonVM Memory Structure create mode 100644 extra/dragon info/Interrupts create mode 100644 extra/dragon info/Memory Map.png create mode 100644 extra/dragon info/regular_expressions.txt create mode 100644 extra/dragon/bios.bin create mode 100644 extra/dragon/cmos.dr create mode 100644 extra/dragon/disk1.dr create mode 100644 extra/dss/bios.dss create mode 100644 extra/dss/handlerTest.dss create mode 100644 extra/dss/mbr.dss create mode 100644 extra/dss/newTest.dss create mode 100644 extra/dss/test.dss create mode 100644 extra/font.bmp create mode 100644 extra/make_and_run create mode 100644 extra/scripts/bios_flash create mode 100644 extra/scripts/load_mbr create mode 100644 src/assembler/Assembler.cpp create mode 100644 src/assembler/Assembler.hpp create mode 100644 src/assembler/assembler_main.cpp create mode 100644 src/debugger/Debugger.cpp create mode 100644 src/debugger/Debugger.hpp create mode 100644 src/debugger/DisassemblyLoader.cpp create mode 100644 src/debugger/DisassemblyLoader.hpp create mode 100644 src/debugger/debugger_main.cpp create mode 100644 src/gui/DebuggerWindow.cpp create mode 100644 src/gui/DebuggerWindow.hpp create mode 100644 src/gui/RawTextRenderer.cpp create mode 100644 src/gui/RawTextRenderer.hpp create mode 100644 src/gui/VirtualConsole.cpp create mode 100644 src/gui/VirtualConsole.hpp create mode 100644 src/gui/VirtualConsoleOutputHandler.cpp create mode 100644 src/gui/VirtualConsoleOutputHandler.hpp create mode 100644 src/gui/Window.cpp create mode 100644 src/gui/Window.hpp create mode 100644 src/gui/widgets/VirtualConsoleWidget.cpp create mode 100644 src/gui/widgets/VirtualConsoleWidget.hpp create mode 100644 src/gui/widgets/Widget.hpp create mode 100644 src/hardware/IMemoryDevice.hpp create mode 100644 src/hardware/MemoryMapper.cpp create mode 100644 src/hardware/MemoryMapper.hpp create mode 100644 src/hardware/VirtualCPU.cpp create mode 100644 src/hardware/VirtualCPU.hpp create mode 100644 src/hardware/VirtualHardDrive.cpp create mode 100644 src/hardware/VirtualHardDrive.hpp create mode 100644 src/hardware/VirtualIODevices.cpp create mode 100644 src/hardware/VirtualIODevices.hpp create mode 100644 src/hardware/VirtualRAM.cpp create mode 100644 src/hardware/VirtualRAM.hpp create mode 100644 src/runtime/ConfigLoader.cpp create mode 100644 src/runtime/ConfigLoader.hpp create mode 100644 src/runtime/DragonRuntime.cpp create mode 100644 src/runtime/DragonRuntime.hpp create mode 100644 src/runtime/runtime_main.cpp create mode 100644 src/tools/GlobalData.hpp create mode 100644 src/tools/SDLInclude.hpp create mode 100644 src/tools/Utils.cpp create mode 100644 src/tools/Utils.hpp create mode 100644 src/tools/tools_main.cpp create mode 100644 tools/CppResourceMaker create mode 100644 tools/build.nr create mode 100644 tools/inc_bnr create mode 100644 tools/inc_bnr.exe diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ea94eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +bin +tools/win-release-tools +win-release \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..006aa2e --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,31 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${default}", + "${workspaceRoot}/../omnia-framework/src" + ], + "defines": [], + "compilerPath": "/usr/bin/clang++", + "cStandard": "c17", + "cppStandard": "c++20", + "intelliSenseMode": "linux-clang-x64" + //"compileCommands": "${workspaceFolder}/compile_commands.json" + }, + { + "name": "Windows", + "includePath": [ + "${default}", + "${workspaceRoot}/../omnia-framework/src" + ], + "defines": [], + "compilerPath": "C:\\msys64\\ucrt64\\bin\\clang++", + "cStandard": "c17", + "cppStandard": "c++20", + "intelliSenseMode": "windows-clang-x64" + //"compileCommands": "${workspaceFolder}/compile_commands.json" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..61116ba --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,143 @@ +{ + "C_Cpp.errorSquiggles": "Enabled", + "files.associations": { + "functional": "cpp", + "thread": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "csetjmp": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "any": "cpp", + "array": "cpp", + "atomic": "cpp", + "hash_map": "cpp", + "barrier": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "cfenv": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "cinttypes": "cpp", + "codecvt": "cpp", + "compare": "cpp", + "complex": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "coroutine": "cpp", + "cstdint": "cpp", + "cuchar": "cpp", + "deque": "cpp", + "forward_list": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "source_location": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "future": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "latch": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "ranges": "cpp", + "scoped_allocator": "cpp", + "semaphore": "cpp", + "shared_mutex": "cpp", + "span": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "syncstream": "cpp", + "typeindex": "cpp", + "typeinfo": "cpp", + "valarray": "cpp", + "variant": "cpp", + "*.rh": "cpp", + "hash_set": "cpp", + "strstream": "cpp", + "expected": "cpp", + "spanstream": "cpp", + "stacktrace": "cpp", + "*.ipp": "cpp", + "format": "cpp", + "stdfloat": "cpp" + }, + + "workbench.colorCustomizations": { + "editor.background": "#131313", + "tab.hoverBackground": "#111", + "tab.inactiveBackground": "#000", + "tab.activeBackground": "#1b1b1b", + "tab.activeBorder": "#bb5400", + "editorIndentGuide.background": "#252525", + "editorWhitespace.foreground": "#683700", + "editor.lineHighlightBorder": "#1a1a1a", + "editor.lineHighlightBackground": "#490050", + "editorBracketMatch.background": "#444", + "editorBracketMatch.border": "#b80318", + "editor.selectionBackground": "#3b3b3b", + "editor.selectionHighlightBackground": "#702900", + "statusBar.background": "#2e2850", + "minimap.background" : "#1b1b1b", + "sideBar.background": "#1b1b1b" + }, + + "workbench.editor.wrapTabs": true, + "todohighlight.isEnable": true, + "todohighlight.keywords": [ + { + "text": "TODO:", + "color": "red", + "backgroundColor": "rgba(0, 0, 0, 0)" + }, + { + "text": "INFO:", + "color": "blue", + "backgroundColor": "rgba(0, 0, 0, 0)" + }, + { + "text": "HACK:", + "color": "purple", + "backgroundColor": "rgba(0, 0, 0, 0)" + } + ], + "editor.insertSpaces": false, + + "workbench.list.openMode": "doubleClick" +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b31c020 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,181 @@ +#Setup +#----------------------------------------------------------------------------------------- +set(CMAKE_CXX_COMPILER "clang++") +set(CMAKE_C_COMPILER "clang") +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +cmake_minimum_required(VERSION 3.18) +project(${PROJ_NAME} LANGUAGES C CXX) +set(CMAKE_BUILD_TYPE Debug) +set(CMAKE_CXX_STANDARD 20) +file(STRINGS "./tools/build.nr" BUILD_NUMBER) +#----------------------------------------------------------------------------------------- + + + +#Variables +#----------------------------------------------------------------------------------------- +set(PROJ_NAME DragonVM) +set(MAJOR_VER 0) +set(MINOR_VER 1) + +message("** Building ${PROJ_NAME} ${MAJOR_VER}.${MINOR_VER}.${BUILD_NUMBER}") +#----------------------------------------------------------------------------------------- + + + +#Sources +#----------------------------------------------------------------------------------------- +list(APPEND INCLUDE_DIRS + ${CMAKE_CURRENT_LIST_DIR}/src + ${CMAKE_CURRENT_LIST_DIR}/../omnia-framework/src +) +list(APPEND RUNTIME_SOURCE_FILES + ${CMAKE_CURRENT_LIST_DIR}/src/runtime/runtime_main.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/runtime/DragonRuntime.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/runtime/ConfigLoader.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/gui/Window.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/widgets/VirtualConsoleWidget.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/RawTextRenderer.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/VirtualConsoleOutputHandler.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/VirtualConsole.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualCPU.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualRAM.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/MemoryMapper.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualIODevices.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualHardDrive.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/tools/Utils.cpp +) +list(APPEND DEBUGGER_SOURCE_FILES + ${CMAKE_CURRENT_LIST_DIR}/src/debugger/debugger_main.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/debugger/DisassemblyLoader.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/debugger/Debugger.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/gui/DebuggerWindow.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/Window.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/widgets/VirtualConsoleWidget.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/RawTextRenderer.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/gui/VirtualConsoleOutputHandler.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualCPU.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualRAM.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/MemoryMapper.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualIODevices.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualHardDrive.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/runtime/DragonRuntime.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/runtime/ConfigLoader.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/assembler/Assembler.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/tools/Utils.cpp +) +list(APPEND ASSEMBLER_SOURCE_FILES + ${CMAKE_CURRENT_LIST_DIR}/src/assembler/assembler_main.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/assembler/Assembler.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualHardDrive.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/tools/Utils.cpp +) +list(APPEND TOOLS_SOURCE_FILES + ${CMAKE_CURRENT_LIST_DIR}/src/tools/tools_main.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/tools/Utils.cpp + + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualHardDrive.cpp +) +#----------------------------------------------------------------------------------------- + + + +#Targets +#----------------------------------------------------------------------------------------- +set(RUNTIME_TARGET dvm) +add_executable(${RUNTIME_TARGET} ${RUNTIME_SOURCE_FILES}) +target_include_directories(${RUNTIME_TARGET} PUBLIC ${INCLUDE_DIRS}) + + +set(DEBUGGER_TARGET ddb) +add_executable(${DEBUGGER_TARGET} ${DEBUGGER_SOURCE_FILES}) +target_include_directories(${DEBUGGER_TARGET} PUBLIC ${INCLUDE_DIRS}) + + +set(ASSEMBLER_TARGET dasm) +add_executable(${ASSEMBLER_TARGET} ${ASSEMBLER_SOURCE_FILES}) +target_include_directories(${ASSEMBLER_TARGET} PUBLIC ${INCLUDE_DIRS}) + + +set(TOOLS_TARGET dtools) +add_executable(${TOOLS_TARGET} ${TOOLS_SOURCE_FILES}) +target_include_directories(${TOOLS_TARGET} PUBLIC ${INCLUDE_DIRS}) + +target_compile_definitions(${RUNTIME_TARGET} PUBLIC BUILD_NR=${BUILD_NUMBER} MAJ_V=${MAJOR_VER} MIN_V=${MINOR_VER} VERSION_STR="${MAJOR_VER}.${MINOR_VER}.${BUILD_NUMBER} - Alpha") + +#TODO: Different flags for Release/Debug +add_compile_options(-O3 -m32 -MMD -MP -Wall -ggdb) +if (UNIX) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath='$ORIGIN'") + #target_link_libraries(${RUNTIME_TARGET} X11 GL) + target_link_libraries(${RUNTIME_TARGET} xcb xcb-randr) + target_link_libraries(${ASSEMBLER_TARGET} xcb xcb-randr) + target_link_libraries(${TOOLS_TARGET} xcb xcb-randr) + target_link_libraries(${DEBUGGER_TARGET} xcb xcb-randr) +endif (UNIX) +if (WIN32) + target_link_libraries(${RUNTIME_TARGET} mingw32) + target_link_libraries(${ASSEMBLER_TARGET} mingw32) + target_link_libraries(${TOOLS_TARGET} mingw32) + target_link_libraries(${DEBUGGER_TARGET} mingw32) +endif (WIN32) +target_link_libraries(${RUNTIME_TARGET} SDL2main SDL2 SDL2_mixer SDL2_image) +target_link_libraries(${DEBUGGER_TARGET} SDL2main SDL2 SDL2_mixer SDL2_image) + +# target_link_libraries(${RUNTIME_TARGET} sfml-system sfml-window sfml-graphics) +#----------------------------------------------------------------------------------------- + + + +#Linking +#----------------------------------------------------------------------------------------- +find_library(OMNIA_STD_LIB + NAMES ostd + HINTS "${CMAKE_CURRENT_SOURCE_DIR}/../omnia-framework/bin/" + NO_CACHE +) +target_link_libraries(${RUNTIME_TARGET} ${OMNIA_STD_LIB}) +target_link_libraries(${DEBUGGER_TARGET} ${OMNIA_STD_LIB}) +target_link_libraries(${ASSEMBLER_TARGET} ${OMNIA_STD_LIB}) +target_link_libraries(${TOOLS_TARGET} ${OMNIA_STD_LIB}) +#----------------------------------------------------------------------------------------- + + + +#BuildNumber Target +#----------------------------------------------------------------------------------------- +if (WIN32) + add_custom_command ( OUTPUT ./tools/build.nr + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/inc_bnr.exe + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tools/ + ) +endif (WIN32) +if (UNIX) + add_custom_command ( OUTPUT ./tools/build.nr + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/inc_bnr + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tools/ + ) +endif (UNIX) + +add_custom_target( + IncBnr ALL + DEPENDS ./tools/build.nr +) +#----------------------------------------------------------------------------------------- + + +add_custom_command(TARGET ${RUNTIME_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/extra/ $ + VERBATIM +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7a94b5 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# DragonVM + +Compile instructions on Windows + +**Step 1:** +download MSYS2 from https://www.msys2.org/ and install it + +**Step 2:** +run MSYS2, and in the terminal run: +``` +pacman -Syuu +pacman -S --needed base-devel mingw-w64-ucrt-x86_64-clang mingw-w64-ucrt-x86_64-gdb mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-sfml mingw-w64-ucrt-x86_64-make mingw-w64-ucrt-x86_64-SDL2 +``` + +**Step 3:** +open a UCRT64/MSYS2 command prompt inside the root directory of the project + +**Step 4:** +execute this command: +``` +./compile +``` diff --git a/compile b/compile new file mode 100644 index 0000000..405fcd6 --- /dev/null +++ b/compile @@ -0,0 +1,16 @@ +#!/bin/bash + +rm -r bin + +if [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + cmake -B bin -S ./ + cd bin + make + cd .. +elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then + cmake -B bin -S ./ -G "MinGW Makefiles" + cd bin + mingw32-make.exe + cd .. + cp ../omnia-framework/bin/libostd.dll ./bin +fi \ No newline at end of file diff --git a/extra/config/testMachine.dvm b/extra/config/testMachine.dvm new file mode 100644 index 0000000..c6c902d --- /dev/null +++ b/extra/config/testMachine.dvm @@ -0,0 +1,3 @@ +Disks = dragon/disk1.dr +Bios = dragon/bios.bin +CMOS = dragon/cmos.dr \ No newline at end of file diff --git a/extra/dragon info/DragonVM Memory Structure b/extra/dragon info/DragonVM Memory Structure new file mode 100644 index 0000000..4b39e89 --- /dev/null +++ b/extra/dragon info/DragonVM Memory Structure @@ -0,0 +1,79 @@ +0x0000 + BIOS (1024 Bytes) +0x03FF +------- +0x0400 + CMOS (128 Bytes) + BootDisk: 0x0010 +0x047F +------- +0x0480 + INTERRUPT VECTOR (512 Bytes) +0x067F +------- +0x0680 + KEYBOARD MAPPING (224 Bytes) +0x075F +------- +0x0760 + MOUSE MAPPING (32 Bytes) +0x077F +------- +0x0780 + BIOS VIDEO MEMORY (3840 Bytes) + 0x000: Clear Color (1 Byte) + 0x001: Palette (1 Byte) + 0x002: UseTransparencyOn0x5 (1 Byte) + 0x003: Signal (1 Byte) + 0x00: Continue + 0x01: Clear Screen + 0x03C - 0xEFF: 3780 Bytes + Structure: + 90x21 characters + 1 Byte ascii + 1 byte: 4 Bits background/4 bits foreground +0x167F +------- +0x1680 + BOOTLOADER (512 Bytes) +0x187F +------- +0x1880 + DISK INTERFACE (128 Bytes) + Read/Write: + Signal: 1 Byte + 0x00: Start Operation + 0x01: Cancel Current Operation + 0xFF: Ignore + Mode: 1 Byte + 0x00: Read + 0x01: Write + Disk: 1 Byte + Sector: 2 Bytes + Address: 2 Bytes + DataSize: 2 Bytes + DataAddress: 2 Byte + ReadOnly: + Status: 1 Byte + 0x00: Free + 0x01: Writing + 0x02: Reading + CurrentDisk: 1 Byte + CurrentSector: 2 Bytes + CurrentAddress: 2 Bytes + RestDataSize: 2 Bytes + SourceData: 2 Bytes + Free: 107 Byte +0x18FF +------- +0x1900 + VIDEO CARD INTERFACE (256 Bytes) +0x19FF +------- +0x1A00 + GENERIC SERIAL INTERFACE (64 Bytes) +0x1A3F +------- +0x1A40 + RAM (58815 Bytes) +0xFFFF diff --git a/extra/dragon info/Interrupts b/extra/dragon info/Interrupts new file mode 100644 index 0000000..0fdc97e --- /dev/null +++ b/extra/dragon info/Interrupts @@ -0,0 +1,70 @@ +Software Interrupts: +==================== +0x20: BIOS Interrupt (Uses R10 register for specific functionality) + 0x00: Set Interrupt Handler + 0x01: Clear Interrupt Handler + 0x10: Disk Interface +0x30: BIOS Video Interrupt (Uses R10 register for specific functionality) + 0x00: Clear Screen + + +Hardware Interrupts: +==================== +0x80: Disk Interface Finished +0x81: BiosVideo Screen Refreshed + + + + +Default Palette BIOS Colors: +==================== +0x0: Black + 0, 0, 0 + #000000 +0x1: Gray + 157, 157, 157 + #9D9D9D +0x2: White + 255, 255, 255 + #FFFFFF +0x3: Red + 190, 38, 51 + #BE2633 +0x4: Pimk + 224, 111, 139 + #E06F8B +OLD -- 0x5: DarkBrown +OLD -- 73, 60, 43 +OLD -- #493C2B +0x5: Transparent + -- +0x6: Brown + 164, 100, 34 + #A46422 +0x7: Orange + 235, 137, 49 + #EB8931 +0x8: Yellow + 247, 226, 107 + #F7E26B +0x9: Dark Green + 47, 80, 42 + #2F502A +0xA: Green + 68, 137, 26 + #44891A +0xB: Slime Green + 163, 206, 39 + #A3CE27 +0xC: Night Blue + 27, 38, 50 + #1B2632 +0xD: Sea Blue + 0, 87, 132 + #005784 +0xE: Sky Blue + 49, 162, 242 + #31A2F2 +0xF: Cloud Blue + 178, 220, 239 + #B2DCEF \ No newline at end of file diff --git a/extra/dragon info/Memory Map.png b/extra/dragon info/Memory Map.png new file mode 100644 index 0000000000000000000000000000000000000000..1ac7c135bde17fea0b4070f9e7e50ad61e71e9a3 GIT binary patch literal 124438 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 WSqqZ0(L@vREZbQEwHYR*6lr$9mq zW*u@1@})oX=W8^@)EpC;u$lVkQ}xM(Nx(QdW4ZjvawvJJ$4zf_QgMT+xP^*)qMC<^ zU%Hu__*KAD-v%6bz2|Pv8<+WpU@vUpa*j&g2N6#G)OHvCMDRRkCvCLN{E{1)2H Q!c4sA1&28_@Q)k#1$XK( register into ACC + jne $_load_mbr_data_block_wait_loop, 0x00 ## If register not Free, loop around and wait + ret + +_calc_interrupt_vector_address: + mov R1, *PP + mul R1, 0x03 ## Multiply the interrupt code by 3 for memory alignment + mov R1, ACC + add R1, INT_VEC_START_ADDRESS ## Add Interrupt Vector base address to Interrupt code + mov RV, ACC + ret + +_set_interrupt_vector_entry: + mov R1, *PP + push R1 + push 1 + call $_calc_interrupt_vector_address + mov R1, RV + dec PP + dec PP + mov ACC, *PP + jeq $_set_interrupt_vector_entry_disable, 0x0000 + movb *R1, 0xFF + jmp $_set_interrupt_vector_entry_end +_set_interrupt_vector_entry_disable: + movb *R1, 0x00 +_set_interrupt_vector_entry_end: + inc R1 + mov *R1, ACC + ret + +.fixed 1024, 0x00 ## BIOS Needs to be 1024 Bytes exactly diff --git a/extra/dss/handlerTest.dss b/extra/dss/handlerTest.dss new file mode 100644 index 0000000..4e8e5e2 --- /dev/null +++ b/extra/dss/handlerTest.dss @@ -0,0 +1,94 @@ +.load 0x1A40 + +##@struct Rectangle { +## width:2, +## height:2, +## x:2, +## y:2 +## +## ## Static properties +## StructSize +##} + +.data + $cursor_pos 0x00, 0x00 + $color 0x02 + $current_video_addr 0x07, 0xBC + + $string "Hello World!!" + + ## $rect => Rectangle + +.code + debug_break +infinite_loop: + push 0 + call $clear_screen + + ## mov $rect.x, 200 + + ## debug_break + + push $string + push 1 + call $print_string + + jmp $infinite_loop + hlt + + +print_string: + mov R1, *PP + dec PP + dec PP + + push R1 + push 1 + call $strlen + mov R2, RV + + push 65 + push 1 + call $print_char + ret + + +strlen: + mov R1, *PP + dec PP + dec PP + mov R2, 0 +_strlen_loop: + movb ACC, *R1 + jeq $_strlen_end_loop, 0 + inc R2 + inc R1 + jmp $_strlen_loop +_strlen_end_loop: + mov RV, R2 + ret + +print_char: + mov R2, *PP + dec PP + dec PP + mov R1, [$current_video_addr] + mov R3, [$cursor_pos] + mul R3, 2 + mov R3, ACC + add R1, R3 + mov R1, ACC + movb *R1, R2 + inc R1 + movb *R1, [$color] + mov R3, [$cursor_pos] + inc R3 + mov [$cursor_pos], R3 + ret + +clear_screen: + mov R10, 0x00 + int 0x30 + ret + +.fixed 512, 0x00 \ No newline at end of file diff --git a/extra/dss/mbr.dss b/extra/dss/mbr.dss new file mode 100644 index 0000000..c8b869f --- /dev/null +++ b/extra/dss/mbr.dss @@ -0,0 +1,7 @@ +.load 0x1680 + +.code + jmp 0x1A40 + hlt + +.fixed 512, 0x00 diff --git a/extra/dss/newTest.dss b/extra/dss/newTest.dss new file mode 100644 index 0000000..52c1b39 --- /dev/null +++ b/extra/dss/newTest.dss @@ -0,0 +1,15 @@ +.load 0x1A40 + +.data + $v1 0x00, 0x01 + $v2 0x00, 0x01 + $v3 0x00, 0x01, 0x00, 0x01, 0x00, 0x01 + +.code + mov R1, 0xAA + mov R2, 0xBB + mov R3, 0xCC + debug_break + hlt + +.fixed 512, 0x00 \ No newline at end of file diff --git a/extra/dss/test.dss b/extra/dss/test.dss new file mode 100644 index 0000000..6f8b098 --- /dev/null +++ b/extra/dss/test.dss @@ -0,0 +1,8 @@ +.load 0x1A40 + +.code + mov R1, 0xABCD + debug_break + hlt + +.fixed 512, 0x00 \ No newline at end of file diff --git a/extra/font.bmp b/extra/font.bmp new file mode 100644 index 0000000000000000000000000000000000000000..65ab7c57b2f3c1ab9eb0094ebf6f63ca7ce653a6 GIT binary patch literal 109962 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 +#include +#include +#include +#include +#include + +#include "../tools/GlobalData.hpp" +#include "../hardware/VirtualHardDrive.hpp" +#include "../tools/Utils.hpp" + +namespace dragon +{ + namespace code + { + ostd::ByteStream Assembler::assembleFromFile(ostd::String fileName) + { + m_rawSource = ""; + m_code.clear(); + m_lines.clear(); + m_rawDataSection.clear(); + m_rawCodeSection.clear(); + m_symbolTable.clear(); + m_labelTable.clear(); + m_disassembly.clear(); + m_fixedSize = 0; + m_fixedFillValue = 0x00; + m_loadAddress = 0x0000; + m_currentDataAddr = 0x0000; + m_dataSize = 0x0000; + ostd::TextFileBuffer file(fileName); + loadSource(file.rawContent()); + removeComments(); + replaceDefines(); + parseSections(); + parseDataSection(); + parseCodeSection(); + replaceLabelRefs(); + combineDataAndCode(); + m_programSize = m_code.size(); + if (m_fixedSize > 0 && m_code.size() > m_fixedSize) + std::cout << "Warning: Fixed size specified but exceeded: (" << (int)m_code.size() << "/" << (int)m_fixedSize << " bytes)\n"; + else if (m_fixedSize > 0 && m_code.size() < m_fixedSize) + { + for (int16_t i = m_code.size(); i < m_fixedSize; i++) + m_code.push_back(m_fixedFillValue); + } + return m_code; + } + + void Assembler::loadSource(ostd::String source) + { + m_rawSource = source; + m_lines = ostd::StringEditor(source).tokenize("\n").getRawData(); + } + + ostd::ByteStream Assembler::assembleToFile(ostd::String sourceFileName, ostd::String binaryFileName) + { + assembleFromFile(sourceFileName); + if (m_code.size() == 0) return { }; + ostd::Utils::saveByteStreamToFile(m_code, binaryFileName); + return m_code; + } + + ostd::ByteStream Assembler::assembleToVirtualDisk(ostd::String fileName, hw::VirtualHardDrive& vhdd, uint32_t address) + { + assembleFromFile(fileName); + if (m_code.size() == 0) return { }; + for (int32_t i = 0; i < m_code.size(); i++) + vhdd.write(address + i, m_code[i]); + return m_code; + } + + bool Assembler::saveDisassemblyToFile(ostd::String fileName) + { + if (m_code.size() == 0 || m_disassembly.size() == 0) return false; + ostd::String header_string = "{ DRAGON_DEBUG_DISASSEMBLY }"; + uint64_t da_size = 0; + da_size += (header_string.length() + 1) * ostd::tTypeSize::BYTE; + da_size += m_disassembly.size() * ostd::tTypeSize::DWORD; //Addresses + da_size += m_disassembly.size() * ostd::tTypeSize::BYTE; //Null Termination + for (auto& da : m_disassembly) + da_size += da.code.length() * ostd::tTypeSize::BYTE; //Code Strings + ostd::serial::SerialIO serializer(da_size, ostd::serial::SerialIO::tEndianness::BigEndian); + ostd::StreamIndex stream_addr = 0; + serializer.w_String(stream_addr, header_string); + stream_addr += (header_string.length() + 1) * ostd::tTypeSize::BYTE; + for (auto& da : m_disassembly) + { + serializer.w_DWord(stream_addr, da.addr); + stream_addr += ostd::tTypeSize::DWORD; + serializer.w_String(stream_addr, da.code); + stream_addr += (da.code.length() + 1) * ostd::tTypeSize::BYTE; + // ostd::ByteStream code_stream = ostd::Utils::stringToByteStream(da.code); + // for (int32_t i = 0; i < code_stream.size(); i++) + // { + // serializer.w_Byte(stream_addr, code_stream[i]); + // stream_addr += ostd::tTypeSize::BYTE; + // } + // serializer.w_Byte(stream_addr, 0x00); //NULL Termination + // stream_addr += ostd::tTypeSize::BYTE; + } + return serializer.saveToFile(fileName); + } + + void Assembler::removeComments(void) + { + std::vector newLines; + ostd::StringEditor lineEdit; + for (auto& line : m_lines) + { + lineEdit = line; + lineEdit.trim(); + if (lineEdit.startsWith("##")) + continue; + if (lineEdit.contains("##")) + { + lineEdit = lineEdit.substr(0, lineEdit.indexOf("##")); + lineEdit.trim(); + } + newLines.push_back(lineEdit.str()); + } + m_lines.clear(); + m_lines = newLines; + } + + void Assembler::replaceDefines(void) + { + struct tDefine { + ostd::String name; + ostd::String value; + }; + auto listContainsDefine = [](const std::vector& list, const ostd::String& name) -> bool { + for (auto& def : list) + { + if (def.name == name) + return true; + } + return false; + }; + std::vector defines; + std::vector newLines; + ostd::StringEditor lineEdit; + ostd::StringEditor tmpLineEdit; + for (auto& line : m_lines) + { + lineEdit = line; + lineEdit.trim(); + tmpLineEdit = lineEdit; + if (tmpLineEdit.toLower().startsWith("@define ")) + { + lineEdit = lineEdit.substr(8); + lineEdit.trim(); + if (!lineEdit.contains(" ")) + { + std::cout << "Invalid @define directive: " << line << "\n"; + return; + } + ostd::String define_name = lineEdit.substr(0, lineEdit.indexOf(" ")); + define_name = ostd::StringEditor(define_name).trim().str(); + ostd::String define_value = lineEdit.substr(lineEdit.indexOf(" ") + 1); + if (listContainsDefine(defines, define_name)) + { + std::cout << "Redefinition of @define value: " << line << "\n"; + return; + } + defines.push_back({ define_name, define_value }); + // std::cout << define_name << ": " << define_value << "\n"; + continue; + } + newLines.push_back(lineEdit.str()); + } + for (auto& line : newLines) + { + ostd::StringEditor lineEdit(line); + for (int32_t i = defines.size() - 1; i >= 0; i--) + lineEdit.replaceAll(defines[i].name, defines[i].value); + line = lineEdit.str(); + } + m_lines.clear(); + m_lines = newLines; + } + + void Assembler::parseSections(void) + { + constexpr uint8_t DATA_SECTION = 0x01; + constexpr uint8_t CODE_SECTION = 0x02; + + uint8_t currentSection = 0x00; + + for (auto& line : m_lines) + { + ostd::StringEditor lineEdit(line); + if (lineEdit.startsWith(".data")) + currentSection = DATA_SECTION; + else if (lineEdit.startsWith(".code")) + currentSection = CODE_SECTION; + else if (lineEdit.startsWith(".fixed ")) + { + if (lineEdit.len() < 8) + { + //TODO: Error + std::cout << "Invalid .fixed value: " << lineEdit << "\n"; + return; + } + lineEdit = lineEdit.substr(7); + lineEdit.trim(); + if (!lineEdit.contains(",")) + { + //TODO: Error + std::cout << "Invalid .fixed value: " << lineEdit << "\n"; + return; + } + ostd::StringEditor fixedSizeEdit = lineEdit.substr(0, lineEdit.indexOf(",")); + fixedSizeEdit.trim(); + lineEdit = lineEdit.substr(lineEdit.indexOf(",") + 1); + lineEdit.trim(); + if (!lineEdit.isNumeric()) + { + //TODO: Error + std::cout << "Invalid .fixed size value: " << lineEdit << "\n"; + return; + } + m_fixedFillValue = lineEdit.toInt(); + if (!fixedSizeEdit.isNumeric()) + { + //TODO: Error + std::cout << "Invalid .fixed fill value: " << lineEdit << "\n"; + return; + } + m_fixedSize = fixedSizeEdit.toInt(); + continue; + } + else if (lineEdit.startsWith(".load ")) + { + if (lineEdit.len() < 7) + { + //TODO: Error + std::cout << "Invalid .load value: " << lineEdit << "\n"; + return; + } + lineEdit = lineEdit.substr(6); + lineEdit.trim(); + if (!lineEdit.isNumeric()) + { + //TODO: Error + std::cout << "Invalid .load value: " << lineEdit << "\n"; + return; + } + m_loadAddress = lineEdit.toInt(); + m_currentDataAddr = m_loadAddress + 3; + continue; + } + else if (lineEdit.startsWith(".")) + { + //TODO: Error + std::cout << "Invalid section: " << lineEdit << "\n"; + return; + } + else + { + if (currentSection == DATA_SECTION) + m_rawDataSection.push_back(line); + else if (currentSection == CODE_SECTION) + m_rawCodeSection.push_back(line); + else + { + //TODO: Error + std::cout << "Invalid section: " << lineEdit << "\n"; + return; + } + } + } + } + + void Assembler::parseDataSection(void) + { + for (auto& line : m_rawDataSection) + { + ostd::StringEditor lineEdit(line); + tSymbol symbol; + if (!lineEdit.startsWith("$") || lineEdit.indexOf(" ") < 2) + { + std::cout << "Invalid data entry: " << lineEdit << "\n"; + continue; + } + ostd::StringEditor symbolName = lineEdit.substr(0, lineEdit.indexOf(" ")); + symbolName.trim(); + lineEdit = lineEdit.substr(lineEdit.indexOf(" ") + 1); + lineEdit.trim(); + if (lineEdit.isNumeric()) + { + // union valueSplit { + // uint16_t value; + // uint8_t bytes[2]; + // } split; + // split.value = lineEdit.toInt(); + // symbol.bytes.push_back(split.bytes[0]); + // symbol.bytes.push_back(split.bytes[1]); + // symbol.address = m_currentDataAddr; + // m_currentDataAddr += 2; + int8_t value = lineEdit.toInt(); + symbol.bytes.push_back(value); + symbol.address = m_currentDataAddr; + m_currentDataAddr += 1; + m_symbolTable[symbolName.str()] = symbol; + continue; + } + else if (lineEdit.startsWith("\"") && lineEdit.endsWith("\"") && lineEdit.len() > 2) + { + lineEdit = lineEdit.substr(1, lineEdit.len() - 1); + for (auto c : lineEdit.str()) + symbol.bytes.push_back((uint8_t)c); + symbol.bytes.push_back((uint8_t)0); //NULL Termination + symbol.address = m_currentDataAddr; + m_currentDataAddr += lineEdit.len(); + m_symbolTable[symbolName.str()] = symbol; + continue; + } + else if (lineEdit.contains(",")) + { + auto tokens = lineEdit.tokenize(","); + if (tokens.count() < 2) + { + std::cout << "Invalid data entry: " << lineEdit << "\n"; + return; + } + for (auto& token : tokens) + { + lineEdit = token; + if (!lineEdit.isNumeric()) + { + std::cout << "Invalid byte in data entry: " << lineEdit << "\n"; + return; + } + symbol.bytes.push_back((uint8_t)lineEdit.toInt()); + } + symbol.address = m_currentDataAddr; + m_currentDataAddr += symbol.bytes.size(); + m_symbolTable[symbolName.str()] = symbol; + continue; + } + else + { + std::cout << "Invalid data entry: " << lineEdit << "\n"; + continue; + } + } + + for (auto& symbol : m_symbolTable) + { + m_dataSize += symbol.second.bytes.size(); + } + } + + void Assembler::parseCodeSection(void) + { + for (auto& line : m_rawCodeSection) + { + ostd::StringEditor lineEdit(line); + uint32_t commaCount = lineEdit.count(","); + uint32_t spaceCount = lineEdit.count(" "); + if (lineEdit.endsWith(":") && commaCount == 0 && spaceCount == 0) //Labels + { + lineEdit = lineEdit.substr(0, lineEdit.len() - 1); + lineEdit.trim(); + m_labelTable["$" + lineEdit.str()] = { { }, 0x0000 }; + continue; + } + } + + tDisassemblyLine _disassembly_line; + for (auto& line : m_rawCodeSection) + { + ostd::StringEditor lineEdit(line); + uint32_t commaCount = lineEdit.count(","); + uint32_t spaceCount = lineEdit.count(" "); + if (lineEdit.endsWith(":") && commaCount == 0 && spaceCount == 0) //Labels + { + // _disassembly_line.addr = m_dataSize + m_loadAddress + m_code.size() + 3; + // _disassembly_line.code = lineEdit.str(); + // m_disassembly.push_back(_disassembly_line); + lineEdit = lineEdit.substr(0, lineEdit.len() - 1); + lineEdit.trim(); + m_labelTable["$" + lineEdit.str()].address = m_dataSize + m_loadAddress + m_code.size() + 3; + continue; + } + else if (commaCount == 0 && spaceCount <= 0) //0 Operands + { + _disassembly_line.addr = m_dataSize + m_loadAddress + m_code.size() + 3; + ostd::StringEditor _tmp_edit(lineEdit.str()); + if (_tmp_edit.toLower().trim().startsWith("debug_")) + parseDebugOperands(lineEdit.str()); + else + parse0Operand(lineEdit.str()); + _disassembly_line.code = lineEdit.str(); + m_disassembly.push_back(_disassembly_line); + continue; + } + else if (commaCount == 0) //1 Operands + { + _disassembly_line.addr = m_dataSize + m_loadAddress + m_code.size() + 3; + lineEdit = replaceSymbols(lineEdit.str()); + parse1Operand(lineEdit.str()); + _disassembly_line.code = lineEdit.str(); + m_disassembly.push_back(_disassembly_line); + continue; + } + else if (commaCount == 1) //2 Operands + { + _disassembly_line.addr = m_dataSize + m_loadAddress + m_code.size() + 3; + lineEdit = replaceSymbols(lineEdit.str()); + parse2Operand(lineEdit.str()); + _disassembly_line.code = lineEdit.str(); + m_disassembly.push_back(_disassembly_line); + continue; + } + else if (commaCount == 2) // 3 Operands + { + _disassembly_line.addr = m_dataSize + m_loadAddress + m_code.size() + 3; + lineEdit = replaceSymbols(lineEdit.str()); + parse3Operand(lineEdit.str()); + _disassembly_line.code = lineEdit.str(); + m_disassembly.push_back(_disassembly_line); + continue; + } + else + { + std::cout << "Invalid syntax: " << lineEdit << "\n"; + return; + } + } + } + + void Assembler::parse0Operand(ostd::String line) + { + if (ostd::StringEditor(line).toLower().startsWith("nop")) + { + m_code.push_back(data::OpCodes::NoOp); + return; + } + else if (ostd::StringEditor(line).toLower().startsWith("ret")) + { + m_code.push_back(data::OpCodes::Ret); + return; + } + else if (ostd::StringEditor(line).toLower().startsWith("rti")) + { + m_code.push_back(data::OpCodes::RetInt); + return; + } + else if (ostd::StringEditor(line).toLower().startsWith("hlt")) + { + m_code.push_back(data::OpCodes::Halt); + return; + } + } + + void Assembler::parseDebugOperands(ostd::String line) + { + if (ostd::StringEditor(line).toLower().startsWith("debug_break")) + { + m_code.push_back(data::OpCodes::DEBUG_Break); + return; + } + } + + void Assembler::parse1Operand(ostd::String line) + { + ostd::StringEditor lineEdit(line); + ostd::StringEditor instEdit(lineEdit.substr(0, lineEdit.indexOf(" "))); + instEdit.trim().toLower(); + ostd::StringEditor opEdit(lineEdit.substr(lineEdit.indexOf(" ") + 1)); + opEdit.trim(); + int16_t word = 0x0000; + if (instEdit.str() == "inc") + { + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + m_code.push_back(data::OpCodes::IncReg); + m_code.push_back((uint8_t)word); + return; + } + else if (instEdit.str() == "dec") + { + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + m_code.push_back(data::OpCodes::DecReg); + m_code.push_back((uint8_t)word); + return; + } + else if (instEdit.str() == "push") + { + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType == eOperandType::Immediate || opType == eOperandType::Label) + { + m_code.push_back(data::OpCodes::PushImm); + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + } + else if (opType == eOperandType::Register) + { + m_code.push_back(data::OpCodes::PushReg); + m_code.push_back((uint8_t)word); + } + else + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register or Immediate required\n"; + exit(0); + } + return; + } + else if (instEdit.str() == "pop") + { + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + m_code.push_back(data::OpCodes::PopReg); + m_code.push_back((uint8_t)word); + return; + } + else if (instEdit.str() == "call") + { + m_code.push_back(0x00); + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType == eOperandType::Immediate || opType == eOperandType::Label) + { + m_code[m_code.size() - 1] = data::OpCodes::CallImm; + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + } + else if (opType == eOperandType::Register) + { + m_code[m_code.size() - 1] = data::OpCodes::CallReg; + m_code.push_back((uint8_t)word); + } + else + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register or Immediate (Label) required\n"; + exit(0); + } + return; + } + else if (instEdit.str() == "not") + { + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + m_code.push_back(data::OpCodes::NotReg); + m_code.push_back((uint8_t)word); + return; + } + else if (instEdit.str() == "jmp") + { + m_code.push_back(data::OpCodes::Jmp); + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType != eOperandType::Label && opType != eOperandType::Immediate) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Immediate (or label) required\n"; + exit(0); + return; + } + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + return; + } + else if (instEdit.str() == "int") + { + eOperandType opType = parseOperand(opEdit.str(), word); + if (opType != eOperandType::Immediate) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Immediate required\n"; + exit(0); + return; + } + m_code.push_back(data::OpCodes::Int); + m_code.push_back((uint8_t)word); + return; + } + else + { + std::cout << "Unknown instruction; " << line << " (" << instEdit << ")\n"; + exit(0); + } + } + + void Assembler::parse2Operand(ostd::String line) + { + ostd::StringEditor lineEdit(line); + ostd::StringEditor instEdit(lineEdit.substr(0, lineEdit.indexOf(" "))); + instEdit.trim().toLower(); + ostd::StringEditor opEdit(lineEdit.substr(lineEdit.indexOf(" ") + 1)); + opEdit.trim(); + int16_t word = 0x0000; + auto st = opEdit.tokenize(","); + if (instEdit.str() == "mov") + { + m_code.push_back(0x00); + int16_t word1 = 0x0000; + int16_t word2 = 0x0000; + eOperandType opType1 = parseOperand(st.next(), word1); + if (opType1 == eOperandType::Register) + { + m_code.push_back((uint8_t)word1); + eOperandType opType2 = parseOperand(st.next(), word2); + switch (opType2) + { + case eOperandType::Immediate: + case eOperandType::Label: + m_code[m_code.size() - 2] = data::OpCodes::MovImmReg; + m_code.push_back((uint8_t)((word2 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word2 & 0x00FF)); + break; + case eOperandType::Register: + m_code[m_code.size() - 2] = data::OpCodes::MovRegReg; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::DerefMemory: + m_code[m_code.size() - 2] = data::OpCodes::MovMemReg; + m_code.push_back((uint8_t)((word2 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word2 & 0x00FF)); + break; + case eOperandType::DerefRegister: + m_code[m_code.size() - 2] = data::OpCodes::MovDerefRegReg; + m_code.push_back((uint8_t)word2); + return; + break; + default: + std::cout << "Invalid operand type; " << line << " (" << opEdit << ")\n"; + exit(0); + break; + } + } + else if (opType1 == eOperandType::DerefMemory) + { + m_code.push_back((uint8_t)((word1 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word1 & 0x00FF)); + eOperandType opType2 = parseOperand(st.next(), word2); + switch (opType2) + { + case eOperandType::Register: + m_code[m_code.size() - 3] = data::OpCodes::MovRegMem; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::DerefRegister: + m_code[m_code.size() - 3] = data::OpCodes::MovDerefRegMem; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::Immediate: + case eOperandType::Label: + m_code[m_code.size() - 3] = data::OpCodes::MovImmMem; + m_code.push_back((uint8_t)((word2 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word2 & 0x00FF)); + break; + default: + std::cout << "Invalid operand type; " << line << " (" << opEdit << ")\n"; + exit(0); + break; + } + } + else if (opType1 == eOperandType::DerefRegister) + { + m_code.push_back((uint8_t)word1); + eOperandType opType2 = parseOperand(st.next(), word2); + switch (opType2) + { + case eOperandType::Register: + m_code[m_code.size() - 2] = data::OpCodes::MovRegDerefReg; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::DerefMemory: + m_code[m_code.size() - 2] = data::OpCodes::MovMemDerefReg; + m_code.push_back((uint8_t)((word2 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word2 & 0x00FF)); + break; + case eOperandType::Immediate: + case eOperandType::Label: + m_code[m_code.size() - 2] = data::OpCodes::MovImmDerefReg; + m_code.push_back((uint8_t)((word2 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word2 & 0x00FF)); + break; + case eOperandType::DerefRegister: + m_code[m_code.size() - 2] = data::OpCodes::MovDerefRegDerefReg; + m_code.push_back((uint8_t)word2); + return; + break; + default: + std::cout << "Invalid operand type; " << line << " (" << opEdit << ")\n"; + exit(0); + break; + } + } + return; + } + else if (instEdit.str() == "movb") + { + m_code.push_back(0x00); + int16_t word1 = 0x0000; + int16_t word2 = 0x0000; + eOperandType opType1 = parseOperand(st.next(), word1); + if (opType1 == eOperandType::DerefMemory) + { + m_code.push_back((uint8_t)((word1 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word1 & 0x00FF)); + eOperandType opType2 = parseOperand(st.next(), word2); + switch (opType2) + { + case eOperandType::Immediate: + m_code[m_code.size() - 3] = data::OpCodes::MovByteImmMem; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::DerefRegister: + m_code[m_code.size() - 3] = data::OpCodes::MovByteDerefRegMem; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::Register: + m_code[m_code.size() - 3] = data::OpCodes::MovByteRegMem; + m_code.push_back((uint8_t)word2); + break; + default: + std::cout << "Invalid operand type; " << line << " (" << opEdit << ")\n"; + exit(0); + break; + } + } + else if (opType1 == eOperandType::DerefRegister) + { + m_code.push_back((uint8_t)word1); + eOperandType opType2 = parseOperand(st.next(), word2); + switch (opType2) + { + case eOperandType::Register: + m_code[m_code.size() - 2] = data::OpCodes::MovByteRegDerefReg; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::DerefMemory: + m_code[m_code.size() - 2] = data::OpCodes::MovByteMemDerefReg; + m_code.push_back((uint8_t)((word2 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word2 & 0x00FF)); + break; + case eOperandType::Immediate: + m_code[m_code.size() - 2] = data::OpCodes::MovByteImmDerefReg; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::DerefRegister: + m_code[m_code.size() - 2] = data::OpCodes::MovByteDerefRegDerefReg; + m_code.push_back((uint8_t)word2); + return; + break; + default: + std::cout << "Invalid operand type; " << line << " (" << opEdit << ")\n"; + exit(0); + break; + } + } + else if (opType1 == eOperandType::Register) + { + m_code.push_back((uint8_t)word1); + eOperandType opType2 = parseOperand(st.next(), word2); + switch (opType2) + { + case eOperandType::Immediate: + m_code[m_code.size() - 2] = data::OpCodes::MovByteImmReg; + m_code.push_back((uint8_t)word2); + break; + case eOperandType::DerefMemory: + m_code[m_code.size() - 2] = data::OpCodes::MovByteMemReg; + m_code.push_back((uint8_t)((word2 & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word2 & 0x00FF)); + break; + case eOperandType::DerefRegister: + m_code[m_code.size() - 2] = data::OpCodes::MovByteDerefRegReg; + m_code.push_back((uint8_t)word2); + return; + break; + default: + std::cout << "Invalid operand type; " << line << " (" << opEdit << ")\n"; + exit(0); + break; + } + } + return; + } + else if (instEdit.str() == "add" || instEdit.str() == "sub" || instEdit.str() == "mul") + { + m_code.push_back(0x00); + eOperandType opType = parseOperand(st.next(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + m_code.push_back((uint8_t)word); + opType = parseOperand(st.next(), word); + + if (opType == eOperandType::Immediate) + { + if (instEdit.str() == "add") m_code[m_code.size() - 2] = data::OpCodes::AddImmReg; + else if (instEdit.str() == "sub") m_code[m_code.size() - 2] = data::OpCodes::SubImmReg; + else if (instEdit.str() == "mul") m_code[m_code.size() - 2] = data::OpCodes::MulImmReg; + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + } + else if (opType == eOperandType::Register) + { + if (instEdit.str() == "add") m_code[m_code.size() - 2] = data::OpCodes::AddRegReg; + else if (instEdit.str() == "sub") m_code[m_code.size() - 2] = data::OpCodes::SubRegReg; + else if (instEdit.str() == "mul") m_code[m_code.size() - 2] = data::OpCodes::MulRegReg; + m_code.push_back((uint8_t)word); + } + else + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Immediate or register required\n"; + exit(0); + return; + } + return; + } + else if (instEdit.str() == "lsh" || instEdit.str() == "rsh" || instEdit.str() == "and" || instEdit.str() == "or" || instEdit.str() == "xor") + { + eOperandType opType = parseOperand(st.next(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + uint8_t regAddr = (uint8_t)word; + opType = parseOperand(st.next(), word); + if (opType == eOperandType::Immediate) + { + if (instEdit.str() == "lsh") m_code.push_back(data::OpCodes::LShiftRegImm); + else if (instEdit.str() == "rsh") m_code.push_back(data::OpCodes::RShiftRegImm); + else if (instEdit.str() == "and") m_code.push_back(data::OpCodes::AndRegImm); + else if (instEdit.str() == "or") m_code.push_back(data::OpCodes::OrRegImm); + else if (instEdit.str() == "xor") m_code.push_back(data::OpCodes::XorRegImm); + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + m_code.push_back((uint8_t)regAddr); + } + else if (opType == eOperandType::Register) + { + if (instEdit.str() == "lsh") m_code.push_back(data::OpCodes::LShiftRegReg); + else if (instEdit.str() == "rsh") m_code.push_back(data::OpCodes::RShiftRegReg); + else if (instEdit.str() == "and") m_code.push_back(data::OpCodes::AndRegReg); + else if (instEdit.str() == "or") m_code.push_back(data::OpCodes::OrRegReg); + else if (instEdit.str() == "xor") m_code.push_back(data::OpCodes::XorRegReg); + m_code.push_back((uint8_t)word); + m_code.push_back((uint8_t)regAddr); + } + else + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register or immediate required\n"; + exit(0); + return; + } + return; + } + else if (instEdit.str() == "jne" || instEdit.str() == "jeq" || instEdit.str() == "jgr" || instEdit.str() == "jls" || instEdit.str() == "jge" || instEdit.str() == "jle") + { + m_code.push_back(0x00); + eOperandType opType = parseOperand(st.next(), word); + if (opType != eOperandType::Immediate && opType != eOperandType::Label) + { + std::cout << (int)opType << "\n"; + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Immediate (or label) required\n"; + exit(0); + return; + } + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + opType = parseOperand(st.next(), word); + + if (opType == eOperandType::Immediate) + { + if (instEdit.str() == "jeq") m_code[m_code.size() - 3] = data::OpCodes::JmpEqImm; + else if (instEdit.str() == "jne") m_code[m_code.size() - 3] = data::OpCodes::JmpNotEqImm; + else if (instEdit.str() == "jgr") m_code[m_code.size() - 3] = data::OpCodes::JmpGrImm; + else if (instEdit.str() == "jls") m_code[m_code.size() - 3] = data::OpCodes::JmpLessImm; + else if (instEdit.str() == "jge") m_code[m_code.size() - 3] = data::OpCodes::JmpGeImm; + else if (instEdit.str() == "jle") m_code[m_code.size() - 3] = data::OpCodes::JmpLeImm; + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + } + else if (opType == eOperandType::Register) + { + if (instEdit.str() == "jeq") m_code[m_code.size() - 3] = data::OpCodes::JmpEqReg; + else if (instEdit.str() == "jne") m_code[m_code.size() - 3] = data::OpCodes::JmpNotEqReg; + else if (instEdit.str() == "jgr") m_code[m_code.size() - 3] = data::OpCodes::JmpGrReg; + else if (instEdit.str() == "jls") m_code[m_code.size() - 3] = data::OpCodes::JmpLessReg; + else if (instEdit.str() == "jge") m_code[m_code.size() - 3] = data::OpCodes::JmpGeReg; + else if (instEdit.str() == "jle") m_code[m_code.size() - 3] = data::OpCodes::JmpLeReg; + m_code.push_back((uint8_t)word); + } + else + { + std::cout << "Invalid operand type: " << line << " (" << opEdit << ") -> Immediate or register required\n"; + exit(0); + return; + } + return; + } + else + { + std::cout << "Unknown instruction: " << line << " (" << instEdit << ")\n"; + exit(0); + } + } + + void Assembler::parse3Operand(ostd::String line) + { + ostd::StringEditor lineEdit(line); + ostd::StringEditor instEdit(lineEdit.substr(0, lineEdit.indexOf(" "))); + instEdit.trim().toLower(); + ostd::StringEditor opEdit(lineEdit.substr(lineEdit.indexOf(" ") + 1)); + opEdit.trim(); + int16_t word = 0x0000; + if (instEdit.str() == "mov") + { + auto st = opEdit.tokenize(","); + eOperandType opType = parseOperand(st.next(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + m_code.push_back(data::OpCodes::MovImmRegOffReg); + m_code.push_back((uint8_t)word); + opType = parseOperand(st.next(), word); + if (opType != eOperandType::DerefMemory) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Pointer required\n"; + exit(0); + return; + } + m_code.push_back((uint8_t)((word & 0xFF00) >> 8)); + m_code.push_back((uint8_t)(word & 0x00FF)); + opType = parseOperand(st.next(), word); + if (opType != eOperandType::Register) + { + std::cout << "Invalid operand type; " << line << " (" << opEdit << ") -> Register required\n"; + exit(0); + return; + } + m_code.push_back((uint8_t)word); + return; + } + else + { + std::cout << "Unknown instruction; " << line << " (" << instEdit << ")\n"; + exit(0); + } + } + + void Assembler::combineDataAndCode(void) + { + uint16_t entryAddr = m_dataSize + m_loadAddress + 3; + std::vector symbols; + ostd::ByteStream newCode; + newCode.push_back(data::OpCodes::Jmp); + newCode.push_back((uint8_t)((entryAddr & 0xFF00) >> 8)); + newCode.push_back((uint8_t)(entryAddr & 0x00FF)); + if (m_dataSize > 0) + m_disassembly.insert(m_disassembly.begin(), { (uint16_t)(m_loadAddress + 3), "[----------DATA_SECTION----------]" }); + m_disassembly.insert(m_disassembly.begin(), { m_loadAddress, ostd::StringEditor("jmp ").add(ostd::Utils::getHexStr(entryAddr, true, 2)).str() }); + for (auto& d : m_symbolTable) + symbols.push_back(d.second); + for (int32_t i = symbols.size() - 1; i >= 0; i--) + { + for (auto& dd : symbols[i].bytes) + newCode.push_back(dd); + } + for (auto& b : m_code) + newCode.push_back(b); + m_code = newCode; + + m_disassembly.push_back({ 0xFFFFFF00, "{ DATA }" }); + for (auto& d : m_symbolTable) + { + m_disassembly.push_back({ d.second.address, d.first }); + } + m_disassembly.push_back({ 0xFFFFFF01, "{ LABELS }" }); + for (auto& l : m_labelTable) + { + m_disassembly.push_back({ l.second.address, l.first }); + } + } + + ostd::String Assembler::replaceSymbols(ostd::String line) + { + ostd::StringEditor lineEdit(line); + for (auto& symbol : m_symbolTable) + lineEdit.replaceAll(symbol.first, ostd::Utils::getHexStr(symbol.second.address, true, 2)); + return lineEdit.str(); + } + + void Assembler::replaceLabelRefs(void) + { + for (auto& label : m_labelTable) + { + for (auto& addr : label.second.references) + { + m_code[addr] = (uint8_t)((label.second.address & 0xFF00) >> 8); + m_code[addr + 1] = (uint8_t)(label.second.address & 0x00FF); + } + } + } + + Assembler::eOperandType Assembler::parseOperand(ostd::String op, int16_t& outOp) + { + ostd::StringEditor opEdit(op); + bool derefReg = false; + if (opEdit.startsWith("*")) + { + opEdit = opEdit.substr(1); + opEdit.trim(); + derefReg = true; + } + int8_t reg = parseRegister(opEdit.str()); + if (reg != data::Registers::Last) + { + outOp = (int16_t)reg; + if (derefReg) + return eOperandType::DerefRegister; + return eOperandType::Register; + } + if (derefReg) + { + std::cout << "Invalid Register Dereferencing: " << op << "\n"; + return eOperandType::Error; + } + if (opEdit.isNumeric()) + { + outOp = (int16_t)opEdit.toInt(); + return eOperandType::Immediate; + } + if (opEdit.startsWith("$")) + { + if (m_labelTable.count(opEdit.str()) == 0) + { + std::cout << "Unknown symbol: " << opEdit << "\n"; + return eOperandType::Error; + } + uint16_t labelAddr = m_labelTable[opEdit.str()].address; + if (labelAddr == 0x0000) + m_labelTable[opEdit.str()].references.push_back(m_code.size()); + outOp = (int16_t)labelAddr; + return eOperandType::Label; + } + if (opEdit.startsWith("{") && opEdit.endsWith("}")) + { + opEdit = opEdit.substr(1, opEdit.len() - 1); + opEdit.trim(); + outOp = (int16_t)ostd::Utils::solveIntegerExpression(opEdit.str()); + return eOperandType::Immediate; + } + if (opEdit.startsWith("[") && opEdit.endsWith("]")) + { + opEdit = opEdit.substr(1, opEdit.len() - 1); + opEdit.trim(); + if (opEdit.startsWith("{") && opEdit.endsWith("}")) + { + opEdit = opEdit.substr(1, opEdit.len() - 1); + opEdit.trim(); + outOp = (int16_t)ostd::Utils::solveIntegerExpression(opEdit.str()); + return eOperandType::DerefMemory; + } + if (!opEdit.isNumeric()) + { + std::cout << "Invalid numeric value: " << opEdit << "\n"; + return eOperandType::Error; + } + outOp = (int16_t)opEdit.toInt(); + return eOperandType::DerefMemory; + } + return eOperandType::Error; + } + + uint8_t Assembler::parseRegister(ostd::String op) + { + ostd::StringEditor opEdit(op); + opEdit.trim().toLower(); + if (opEdit.str() == "r1") return data::Registers::R1; + if (opEdit.str() == "r2") return data::Registers::R2; + if (opEdit.str() == "r3") return data::Registers::R3; + if (opEdit.str() == "r4") return data::Registers::R4; + if (opEdit.str() == "r5") return data::Registers::R5; + if (opEdit.str() == "r6") return data::Registers::R6; + if (opEdit.str() == "r7") return data::Registers::R7; + if (opEdit.str() == "r8") return data::Registers::R8; + if (opEdit.str() == "r9") return data::Registers::R9; + if (opEdit.str() == "r10") return data::Registers::R10; + if (opEdit.str() == "acc") return data::Registers::ACC; + if (opEdit.str() == "fp") return data::Registers::FP; + if (opEdit.str() == "sp") return data::Registers::SP; + if (opEdit.str() == "ip") return data::Registers::IP; + if (opEdit.str() == "pp") return data::Registers::PP; + if (opEdit.str() == "rv") return data::Registers::RV; + if (opEdit.str() == "fl") return data::Registers::FL; + return data::Registers::Last; + } + + void Assembler::tempPrint(void) + { + for (auto& symbol : m_symbolTable) + { + std::cout << symbol.first << "\n"; + std::cout << ostd::Utils::getHexStr(symbol.second.address, true, 2) << "\n"; + for (auto& b : symbol.second.bytes) + std::cout << ostd::Utils::getHexStr(b, true, 1) << " "; + std::cout << "\n\n"; + } + + std::cout << "Fixed Size: " << (int)m_fixedSize << "\n"; + std::cout << "Fixed Fill: " << ostd::Utils::getHexStr(m_fixedFillValue, true, 1) << "\n"; + std::cout << "Load Address: " << ostd::Utils::getHexStr(m_loadAddress, true, 2) << "\n"; + std::cout << "Data Size: " << (int)m_dataSize << "\n"; + std::cout << "Code Start Address: " << ostd::Utils::getHexStr(m_dataSize + m_loadAddress + 3, true, 2) << "\n"; + std::cout << "Program Size: " << (int)m_programSize << "\n"; + + for (auto& label : m_labelTable) + { + std::cout << label.first << " " << ostd::Utils::getHexStr(label.second.address, true, 2) << "\n"; + } + + ostd::ConsoleOutputHandler out; + ostd::Utils::printByteStream(m_code, 0x0000, 16, 8, out); + std::cout << "\n\n\n\n"; + } + } +} \ No newline at end of file diff --git a/src/assembler/Assembler.hpp b/src/assembler/Assembler.hpp new file mode 100644 index 0000000..4cbb58b --- /dev/null +++ b/src/assembler/Assembler.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include + +namespace dragon +{ + namespace hw + { + class VirtualHardDrive; + } + namespace code + { + class Assembler + { + public: struct tDisassemblyLine { + uint32_t addr = 0; + ostd::String code = ""; + }; + public: struct tSymbol + { + std::vector bytes; + uint16_t address { 0 }; + }; + public: struct tLabel + { + std::vector references; + uint16_t address { 0 }; + }; + public: enum class eOperandType { + Register = 0, + Immediate, + DerefMemory, + DerefRegister, + Label, + + Error + }; + + public: + static ostd::ByteStream assembleFromFile(ostd::String fileName); + static void loadSource(ostd::String source); + static ostd::ByteStream assembleToFile(ostd::String sourceFileName, ostd::String binaryFileName); + static ostd::ByteStream assembleToVirtualDisk(ostd::String fileName, hw::VirtualHardDrive& vhdd, uint32_t address); + static bool saveDisassemblyToFile(ostd::String fileName); + + static void tempPrint(void); + + private: + static void removeComments(void); + static void replaceDefines(void); + static void parseSections(void); + static void parseDataSection(void); + static void parseCodeSection(void); + static void parseDebugOperands(ostd::String line); + static void parse0Operand(ostd::String line); + static void parse1Operand(ostd::String line); + static void parse2Operand(ostd::String line); + static void parse3Operand(ostd::String line); + static void combineDataAndCode(void); + + static ostd::String replaceSymbols(ostd::String line); + static void replaceLabelRefs(void); + static eOperandType parseOperand(ostd::String op, int16_t& outOp); + static uint8_t parseRegister(ostd::String op); + + private: + inline static ostd::String m_rawSource { "" }; + inline static ostd::ByteStream m_code; + inline static std::vector m_lines; + inline static std::vector m_rawDataSection; + inline static std::vector m_rawCodeSection; + inline static std::unordered_map m_symbolTable; + inline static std::unordered_map m_labelTable; + inline static uint16_t m_fixedSize { 0 }; + inline static uint8_t m_fixedFillValue { 0x00 }; + inline static uint16_t m_loadAddress { 0x0000 }; + inline static uint16_t m_currentDataAddr { 0x0000 }; + inline static uint16_t m_dataSize { 0x0000 }; + inline static uint16_t m_programSize { 0x0000 }; + + inline static std::vector m_disassembly; + }; + } +} \ No newline at end of file diff --git a/src/assembler/assembler_main.cpp b/src/assembler/assembler_main.cpp new file mode 100644 index 0000000..48cd970 --- /dev/null +++ b/src/assembler/assembler_main.cpp @@ -0,0 +1,60 @@ +#include "Assembler.hpp" +#include +#include + +ostd::legacy::ConsoleOutputHandler out; + + +struct tCommandLineArgs +{ + ostd::String source_file_path = ""; + ostd::String dest_file_path = ""; + bool save_disassembly = false; + bool verbose = false; + ostd::String disassembly_file_path = ""; +}; + +int main(int argc, char** argv) +{ + tCommandLineArgs args; + if (argc < 4) + { + out.col("red").p("Error: too few arguments.").nl(); + out.col("red").p(" Usage: ./dasm -o [...options...]").resetColors().nl(); + return 1; + } + else + { + args.source_file_path = argv[1]; + for (int32_t i = 2; i < argc; i++) + { + ostd::StringEditor edit(argv[i]); + if (edit.str() == "-o") + { + if (i == argc - 1) + break; //TODO: Warning + i++; + args.dest_file_path = argv[i]; + } + else if (edit.str() == "--save-disassembly") + { + if (i == argc - 1) + break; //TODO: Warning + i++; + args.disassembly_file_path = argv[i]; + args.save_disassembly = true; + } + else if (edit.str() == "--verbose") + args.verbose = true; + } + } + + dragon::code::Assembler::assembleToFile(args.source_file_path, args.dest_file_path); + if (args.verbose) + dragon::code::Assembler::tempPrint(); + + if (args.save_disassembly) + dragon::code::Assembler::saveDisassemblyToFile(args.disassembly_file_path); + + return 0; +} \ No newline at end of file diff --git a/src/debugger/Debugger.cpp b/src/debugger/Debugger.cpp new file mode 100644 index 0000000..cfaa7a2 --- /dev/null +++ b/src/debugger/Debugger.cpp @@ -0,0 +1,829 @@ +#include "Debugger.hpp" +#include "../runtime/DragonRuntime.hpp" +#include "DisassemblyLoader.hpp" +#include + +namespace dragon +{ + //Debugger::Utils + DisassemblyList Debugger::Utils::findCodeRegion(const DisassemblyList& code, uint16_t address, uint16_t codeRegionMargin) + { + if (code.size() <= (codeRegionMargin * 2) + 1) return code; + std::vector codeRegion; + uint16_t start = 0; + uint16_t end = (codeRegionMargin * 2); + for (int32_t i = 0; i < code.size(); i++) + { + if (code[i].addr != address) continue; + if (i + 1 <= codeRegionMargin) break; + if (code.size() - (i + 1) < codeRegionMargin) + { + end = code.size() - 1; + start = end - ((codeRegionMargin * 2) + 1); + break; + } + start = i - codeRegionMargin; + end = i + codeRegionMargin; + break; + } + for (int16_t i = start; i <= end; i++) + codeRegion.push_back(code[i]); + return codeRegion; + } + + ostd::String Debugger::Utils::findSymbol(const DisassemblyList& labels, uint16_t address) + { + for (auto& label : labels) + { + if (label.addr == address) + return label.code; + } + return ""; + } + + uint16_t Debugger::Utils::findSymbol(const DisassemblyList& labels, const ostd::StringEditor& symbol) + { + for (auto& label : labels) + { + if (label.code == symbol.str()) + return label.addr; + } + return 0x0000; + } + + bool Debugger::Utils::isValidLabelNameChar(char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_'); + } + + ostd::StringEditor Debugger::Utils::fillString(const ostd::StringEditor& str, char fill, int32_t totalLength) + { + int32_t fillLen = totalLength - str.len(); + if (fillLen < 1) return str; + ostd::StringEditor newStr = str; + newStr.add(ostd::Utils::duplicateChar(fill, fillLen)); + return newStr; + } + + + + + //Debugger::Display + void Debugger::Display::colorizeInstructionBody(const ostd::String& instBody, bool currentLine, const DisassemblyList& labelList) + { + ostd::RegexRichString rgxrstr(instBody); + rgxrstr.fg("\\{|\\}|\\+|\\*|\\-|\\/|\\(|\\)|\\[|\\]", "Red"); //Operators + rgxrstr.fg("0x[0-9A-Fa-f]+|0b[0-1]+|(? ").col(ostd::legacy::ConsoleCol::White); + } + + void Debugger::Display::printStep(void) + { + out.clear(); + int32_t codeRegionSpan = 15; + auto codeRegion = Utils::findCodeRegion(debugger.code, debugger.currentAddress, codeRegionSpan); + for (int32_t i = 0; i < codeRegion.size(); i++) + { + auto& _da = codeRegion[i]; + bool currentLine = _da.addr == debugger.currentAddress; + ostd::String label = Utils::findSymbol(debugger.labels, _da.addr); + bool specialSection = _da.code.starts_with("[") &&_da.code.ends_with("]"); + if (label.length() < debugger.labelLineLength) + { + label += ostd::Utils::duplicateChar(' ', debugger.labelLineLength - label.length()); + } + else if (label.length() > debugger.labelLineLength) + { + ostd::StringEditor edit(label); + edit = edit.substr(0, debugger.labelLineLength - 3); + edit.add("..."); + label = edit.str(); + } + out.col(ostd::legacy::ConsoleCol::Gray).p(label).p(" "); + if (currentLine) + { + out.col(ostd::legacy::ConsoleCol::Black).col(ostd::legacy::ConsoleCol::OnYellow).p(ostd::Utils::getHexStr(_da.addr, true, 2)).p(" ").reset();; + } + else + { + if (specialSection) + out.col(ostd::legacy::ConsoleCol::Cyan); + else + out.col(ostd::legacy::ConsoleCol::BrightGray); + out.p(ostd::Utils::getHexStr(_da.addr, true, 2)).p(" "); + } + if (specialSection) + out.col(ostd::legacy::ConsoleCol::Cyan).p(_da.code).nl(); + else + colorCodeInstructions(_da.code, currentLine, debugger.labels); + out.reset(); + } + int32_t cw = ostd::Utils::getConsoleWidth(); + ostd::String str = ostd::Utils::duplicateChar('#', cw); + out.col(ostd::legacy::ConsoleCol::OnYellow).col(ostd::legacy::ConsoleCol::Black).p(str).reset().nl(); + } + + void Debugger::Display::printDiff(void) + { + out.clear(); + + ostd::StringEditor str; + str.add("|===============|================PREV================|================CURR================|=====|====REG====|====REG====|"); + str.add("\n"); + str.add("| InstAddr: |*%PREV_INST_ADDR%*******************|*%CURR_INST_ADDR%*******************| R1 |*%PREV_R1%*|*%CURR_R1%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| Code: | ---- |*%CURR_CODE%************************| R2 |*%PREV_R2%*|*%CURR_R2%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| StackFrame: |*%PREV_STACK_FRAME%*****************|*%CURR_STACK_FRAME%*****************| R3 |*%PREV_R3%*|*%CURR_R3%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| DBG BRK: |*%PREV_DBG_BRK%*********************|*%CURR_DBG_BRK%*********************| R4 |*%PREV_R4%*|*%CURR_R4%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| INT Handler: |*%PREV_INT_HANDLER%*****************|*%CURR_INT_HANDLER%*****************| R5 |*%PREV_R5%*|*%CURR_R5%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| BIOS Mode: |*%PREV_BIOS_MODE%*******************|**%CURR_BIOS_MODE%******************| R6 |*%PREV_R6%*|*%CURR_R6%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| IP: |*%PREV_IP%**************************|**%CURR_IP%*************************| R7 |*%PREV_R7%*|*%CURR_R7%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| SP: |*%PREV_SP%**************************|**%CURR_SP%*************************| R8 |*%PREV_R8%*|*%CURR_R8%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| FP: |*%PREV_FP%**************************|**%CURR_FP%*************************| R9 |*%PREV_R9%*|*%CURR_R9%*|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|-----|-----------|-----------|"); + str.add("\n"); + str.add("| RV: |*%PREV_RV%**************************|**%CURR_RV%*************************| R10 |*%PREV_R10%|*%CURR_R10%|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|=====|===========|===========|"); + str.add("\n"); + str.add("| PP: |*%PREV_PP%**************************|**%CURR_PP%*************************|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|"); + str.add("\n"); + str.add("| FL: |*%PREV_FL%**************************|**%CURR_FL%*************************|"); + str.add("\n"); + str.add("|---------------|------------------------------------|------------------------------------|"); + str.add("\n"); + str.add("| ACC: |*%PREV_ACC%*************************|**%CURR_ACC%************************|"); + str.add("\n"); + str.add("|===============|====================================|====================================|"); + + + str.replaceAll("*", ""); + int32_t item_len = 36; + const dragon::DragonRuntime::tMachineDebugInfo& minfo = dragon::DragonRuntime::getMachineInfoDiff(); + ostd::StringEditor tmp = " ", tmpStyle = ""; + + //Instruction Address + { + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionAddress, true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_INST_ADDR%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionAddress, true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionAddress != minfo.previousInstructionAddress) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_INST_ADDR%", tmpStyle.str()); + } + + //Code + { + tmp = " ", tmpStyle = ""; + ostd::StringEditor prevCode = " ("; + prevCode.add(minfo.previousInstructionOpCode).add(") "); + for (int32_t i = 0; i < minfo.previousInstructionFootprintSize; i++) + prevCode.add(ostd::Utils::getHexStr(minfo.previousInstructionFootprint[i], false, 1)).add(" "); + tmp.add(prevCode.str()); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Black,background:BrightGray]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_CODE%", tmpStyle.str()); + + // tmp = " "; + // ostd::StringEditor currCode = " ("; + // currCode.add(minfo.currentInstructionOpCode).add(") "); + // for (int32_t i = 0; i < minfo.currentInstructionFootprintSize; i++) + // currCode.add(ostd::Utils::getHexStr(minfo.currentInstructionFootprint[i], false, 1)).add(" "); + // tmp.add(currCode.str()); + // tmp = fillString(tmp, ' ', item_len); + // if (currCode.str() != prevCode.str()) + // tmpStyle = "[@@style foreground:Black,background:Yellow]"; + // else + // tmpStyle = "[@@style foreground:Blue]"; + // tmpStyle.add(tmp.str()).add("[@@/]"); + // str.replaceAll("%CURR_CODE%", tmpStyle.str()); + } + + //Stack Frame + { + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionStackFrameSize, true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_STACK_FRAME%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionStackFrameSize, true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionStackFrameSize != minfo.previousInstructionStackFrameSize) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_STACK_FRAME%", tmpStyle.str()); + } + + //Debug Break + { + tmp = " ", tmpStyle = ""; + tmp.add(STR_BOOL(minfo.previousInstructionDebugBreak)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_DBG_BRK%", tmpStyle.str()); + + tmp = " "; + tmp.add(STR_BOOL(minfo.currentInstructionDebugBreak)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionDebugBreak != minfo.previousInstructionDebugBreak) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_DBG_BRK%", tmpStyle.str()); + } + + //INT Handler + { + tmp = " ", tmpStyle = ""; + tmp.add(STR_BOOL(minfo.previousInstructionInterruptHandler)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_INT_HANDLER%", tmpStyle.str()); + + tmp = " "; + tmp.add(STR_BOOL(minfo.currentInstructionInterruptHandler)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionInterruptHandler != minfo.previousInstructionInterruptHandler) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_INT_HANDLER%", tmpStyle.str()); + } + + //Bios Mode + { + tmp = " ", tmpStyle = ""; + tmp.add(STR_BOOL(minfo.previousInstructionBiosMode)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_BIOS_MODE%", tmpStyle.str()); + + tmp = " "; + tmp.add(STR_BOOL(minfo.currentInstructionBiosMode)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionBiosMode != minfo.previousInstructionBiosMode) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_BIOS_MODE%", tmpStyle.str()); + } + + //System Registers + { + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::IP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_IP%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::IP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::IP] != minfo.previousInstructionRegisters[dragon::data::Registers::IP]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_IP%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::SP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_SP%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::SP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::SP] != minfo.previousInstructionRegisters[dragon::data::Registers::SP]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_SP%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::FP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_FP%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::FP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::FP] != minfo.previousInstructionRegisters[dragon::data::Registers::FP]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_FP%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::RV], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_RV%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::RV], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::RV] != minfo.previousInstructionRegisters[dragon::data::Registers::RV]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_RV%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::PP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_PP%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::PP], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::PP] != minfo.previousInstructionRegisters[dragon::data::Registers::PP]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_PP%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::FL], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_FL%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::FL], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::FL] != minfo.previousInstructionRegisters[dragon::data::Registers::FL]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_FL%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::ACC], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_ACC%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::ACC], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::ACC] != minfo.previousInstructionRegisters[dragon::data::Registers::ACC]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_ACC%", tmpStyle.str()); + } + + item_len = 11; + //General Registers + { + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R1], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R1%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R1], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R1] != minfo.previousInstructionRegisters[dragon::data::Registers::R1]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R1%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R2], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R2%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R2], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R2] != minfo.previousInstructionRegisters[dragon::data::Registers::R2]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R2%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R3], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R3%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R3], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R3] != minfo.previousInstructionRegisters[dragon::data::Registers::R3]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R3%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R4], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R4%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R4], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R4] != minfo.previousInstructionRegisters[dragon::data::Registers::R4]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R4%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R5], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R5%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R5], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R5] != minfo.previousInstructionRegisters[dragon::data::Registers::R5]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R5%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R6], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R6%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R6], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R6] != minfo.previousInstructionRegisters[dragon::data::Registers::R6]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R6%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R7], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R7%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R7], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R7] != minfo.previousInstructionRegisters[dragon::data::Registers::R7]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R7%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R8], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R8%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R8], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R8] != minfo.previousInstructionRegisters[dragon::data::Registers::R8]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R8%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R9], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R9%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R9], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R9] != minfo.previousInstructionRegisters[dragon::data::Registers::R9]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R9%", tmpStyle.str()); + + + + + tmp = " ", tmpStyle = ""; + tmp.add(ostd::Utils::getHexStr(minfo.previousInstructionRegisters[dragon::data::Registers::R10], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%PREV_R10%", tmpStyle.str()); + + tmp = " "; + tmp.add(ostd::Utils::getHexStr(minfo.currentInstructionRegisters[dragon::data::Registers::R10], true, 2)); + tmp = Utils::fillString(tmp, ' ', item_len); + if (minfo.currentInstructionRegisters[dragon::data::Registers::R10] != minfo.previousInstructionRegisters[dragon::data::Registers::R10]) + tmpStyle = "[@@style foreground:Black,background:BrightRed]"; + else + tmpStyle = "[@@style foreground:Blue]"; + tmpStyle.add(tmp.str()).add("[@@/]"); + str.replaceAll("%CURR_R10%", tmpStyle.str()); + } + + + + ostd::RegexRichString rgxstr(str); + rgxstr.fg("InstAddr|Code|StackFrame|DBG BRK|INT Handler|BIOS Mode", "Magenta"); + rgxstr.fg("IP|SP|FP|RV|PP|FL|ACC", "Cyan"); + rgxstr.fg("R10|R2|R3|R4|R5|R6|R7|R8|R9|R1", "BrightGreen"); + rgxstr.fg("PREV", "Red"); + rgxstr.fg("CURR", "Green"); + out.pStyled(rgxstr); + + out.nl().nl().col(ostd::legacy::ConsoleCol::Yellow).p("Pres to continue execution...").nl().reset(); + std::cin.get(); + } + + void Debugger::Display::printTrackedAddresses(const std::vector& trackedAddresses) + { + + } + + ostd::StringEditor Debugger::Display::changeScreen(void) + { + if (debugger.command.str() == "diff") + { + printDiff(); + printStep(); + return getCommandInput(); + } + else if (debugger.command.str() == "tracker") + { + printTrackedAddresses(debugger.trackedAddresses); + printStep(); + return getCommandInput(); + } + return ""; + } + + + + + //Debugger + void Debugger::processErrors(void) + { + if (!dragon::DragonRuntime::hasError()) return; + while (dragon::data::ErrorHandler::hasError()) + { + auto err = dragon::data::ErrorHandler::popError(); + out.nl().col(ostd::legacy::ConsoleCol::Red).p("Error ").p(ostd::Utils::getHexStr(err.code, true, 8)).p(": ").p(err.text).nl(); + } + debugger.args.step_exec = true; + } + + int32_t Debugger::loadArguments(int argc, char** argv) + { + if (argc < 2) + { + out.col("red").p("Error, too few arguments.").resetColors().nl(); + return 128; + } + else + { + debugger.args.machine_config_path = argv[1]; + for (int32_t i = 2; i < argc; i++) + { + ostd::StringEditor edit(argv[i]); + if (edit.str() == "--verbose-load") + debugger.args.verbose_load = true; + else if (edit.str() == "--step-exec") + debugger.args.step_exec = true; + else if (edit.str() == "--track-step-diff") + debugger.args.track_step_diff = true; + else if (edit.str() == "--hide-vdisplay") + debugger.args.hide_virtual_display = true; + else if (edit.str() == "--auto-start") + debugger.args.auto_start_debug = true; + else if (edit.str() == "--force-load") + { + if ((argc - 1) - i < 2) + break; //TODO: Warning + i++; + debugger.args.force_load_file = argv[i]; + i++; + edit = argv[i]; + if (!edit.isNumeric()) + continue; //TODO: Error + debugger.args.force_load_mem_offset = (uint16_t)edit.toInt(); + debugger.args.force_load = true; + } + } + } + return 0; + } + + int32_t Debugger::initRuntime(void) + { + int32_t init_state = dragon::DragonRuntime::initMachine(debugger.args.machine_config_path, debugger.args.verbose_load, debugger.args.track_step_diff, debugger.args.hide_virtual_display); + if (init_state != 0) return init_state; //TODO: Error + + if (debugger.args.force_load) + dragon::DragonRuntime::forceLoad(debugger.args.force_load_file, debugger.args.force_load_mem_offset); + + dragon::DisassemblyLoader::loadDirectory(debugger.disassemblyDirectory.str()); + debugger.code = dragon::DisassemblyLoader::getCodeTable(); + debugger.labels = dragon::DisassemblyLoader::getLabelTable(); + debugger.data = dragon::DisassemblyLoader::getDataTable(); + + return 0; + } + + ostd::StringEditor Debugger::getCommandInput(void) + { + ostd::String cmd; + std::getline(std::cin, cmd); + return cmd; + } + +} \ No newline at end of file diff --git a/src/debugger/Debugger.hpp b/src/debugger/Debugger.hpp new file mode 100644 index 0000000..24acb47 --- /dev/null +++ b/src/debugger/Debugger.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include "../assembler/Assembler.hpp" + +namespace dragon +{ + typedef std::vector DisassemblyList; + + class Debugger + { + public: struct tCommandLineArgs + { + inline tCommandLineArgs(void) { } + ostd::String machine_config_path = ""; + bool verbose_load = false; + bool force_load = false; + bool step_exec = false; + bool track_step_diff = false; + bool auto_start_debug = false; + bool hide_virtual_display = false; + ostd::String force_load_file = ""; + uint16_t force_load_mem_offset = 0x00; + }; + public: struct tDebuggerData + { + inline tDebuggerData(void) { } + tCommandLineArgs args; + DisassemblyList code; + DisassemblyList labels; + DisassemblyList data; + std::vector trackedAddresses; + ostd::StringEditor command; + int32_t labelLineLength { 20 }; + uint16_t currentAddress { 0 }; + bool userQuit { false }; + ostd::StringEditor disassemblyDirectory { "disassembly" }; + }; + public: class Utils + { + public: + static DisassemblyList findCodeRegion(const DisassemblyList& code, uint16_t address, uint16_t codeRegionMargin); + static ostd::String findSymbol(const DisassemblyList& labels, uint16_t address); + static uint16_t findSymbol(const DisassemblyList& labels, const ostd::StringEditor& symbol); + static bool isValidLabelNameChar(char c); + static ostd::StringEditor fillString(const ostd::StringEditor& str, char fill, int32_t totalLength); //TODO: Implement in omnia-framework + }; + public: class Display + { + public: + static void colorizeInstructionBody(const ostd::String& instBody, bool currentLine, const DisassemblyList& labelList); + static void colorCodeInstructions(const ostd::String& inst, bool currentLine, const DisassemblyList& labelList); + static void printPrompt(void); + static void printStep(void); + static void printDiff(void); + static void printTrackedAddresses(const std::vector& trackedAddresses); + static ostd::StringEditor changeScreen(void); + }; + public: + static void processErrors(void); + static int32_t loadArguments(int argc, char** argv); + static int32_t initRuntime(void); + static ostd::StringEditor getCommandInput(void); + static inline tDebuggerData& data(void) { return debugger; } + static inline ostd::legacy::ConsoleOutputHandler& output(void) { return out; } + + private: + inline static tDebuggerData debugger; + inline static ostd::legacy::ConsoleOutputHandler out; + }; +} \ No newline at end of file diff --git a/src/debugger/DisassemblyLoader.cpp b/src/debugger/DisassemblyLoader.cpp new file mode 100644 index 0000000..f0f444f --- /dev/null +++ b/src/debugger/DisassemblyLoader.cpp @@ -0,0 +1,137 @@ +#include "DisassemblyLoader.hpp" + +#include +#include + +#include "../tools/Utils.hpp" + +namespace dragon +{ + const DisassemblyTable DisassemblyTable::DefaultObject; + + void DisassemblyTable::init(const ostd::String& filePath) + { + ostd::ByteStream stream; + if (!ostd::Utils::loadByteStreamFromFile(filePath, stream)) + { + m_initialized = false; + return; + } + load_data(stream); + } + + void DisassemblyTable::load_data(ostd::ByteStream& stream) + { + constexpr int32_t MODE_CODE = 0, MODE_DATA = 1, MODE_LABELS = 2; + int32_t mode = MODE_CODE; + ostd::serial::SerialIO serializer(stream, ostd::serial::SerialIO::tEndianness::BigEndian); + ostd::StreamIndex addr = 0; + int32_t line_addr = 0; + int8_t line_code_char = 0; + ostd::String header_string = ""; + serializer.r_NullTerminatedString(0, header_string); + if (header_string != "{ DRAGON_DEBUG_DISASSEMBLY }") return; + addr += (header_string.length() + 1) * ostd::tTypeSize::BYTE; + while (addr < serializer.size()) + { + serializer.r_DWord(addr, line_addr); + addr += ostd::tTypeSize::DWORD; + // ostd::ByteStream str_stream; + // serializer.r_Byte(addr, line_code_char); + // addr += ostd::tTypeSize::BYTE; + // do + // { + // str_stream.push_back(line_code_char); + // serializer.r_Byte(addr, line_code_char); + // addr += ostd::tTypeSize::BYTE; + // } while(line_code_char != 0); + // ostd::String code_line = ostd::Utils::byteStreamToString(str_stream); + ostd::String code_line = ""; + serializer.r_NullTerminatedString(addr, code_line); + addr += (code_line.length() + 1) * ostd::tTypeSize::BYTE; + if (code_line == "{ DATA }") + { + mode = MODE_DATA; + continue; + } + else if (code_line == "{ LABELS }") + { + mode = MODE_LABELS; + continue; + } + else if (code_line == "[----------DATA_SECTION----------]") + { + continue; //TODO: Store addresses for various data sections + } + + code::Assembler::tDisassemblyLine line; + line.addr = line_addr; + line.code = code_line; + if (mode == MODE_CODE) + { + ostd::StringEditor codeEdit(line.code); + codeEdit.trim(); + if (codeEdit.contains(" ")) + { + ostd::StringEditor part1 = codeEdit.substr(0, codeEdit.indexOf(" ")); + ostd::StringEditor part2 = codeEdit.substr(codeEdit.indexOf(" ") + 1); + part1.trim(); + part2.trim(); + int32_t opCodeLen = 10; + if (part1.len() < opCodeLen) + { + codeEdit = part1.str() + ostd::Utils::duplicateChar(' ', opCodeLen - part1.len()) + part2.str(); + line.code = codeEdit.str(); + } + } + m_code.push_back(line); + } + else if (mode == MODE_DATA) + m_data.push_back(line); + else if (mode == MODE_LABELS) + m_labels.push_back(line); + } + m_initialized = true; + } + + + + void DisassemblyLoader::loadDirectory(const ostd::String& directoryPath) + { + auto list = ostd::Utils::listFilesInDirectory(directoryPath); + for (auto& path : list) + loadFile(path.string()); + } + + const DisassemblyTable& DisassemblyLoader::loadFile(const ostd::String& filePath) + { + DisassemblyTable table(filePath); + if (!table.isInitialized()) return DisassemblyTable::DefaultObject; //TODO: Error + m_tables.push_back(table); + return m_tables[m_tables.size() - 1]; + } + + std::vector DisassemblyLoader::getCodeTable(void) + { + std::vector fullTable; + for (auto& table : m_tables) + fullTable.insert(fullTable.end(), table.getCodeTable().begin(), table.getCodeTable().end()); + return fullTable; + } + + std::vector DisassemblyLoader::getDataTable(void) + { + std::vector fullTable; + for (auto& table : m_tables) + fullTable.insert(fullTable.end(), table.getDataTable().begin(), table.getDataTable().end()); + return fullTable; + } + + std::vector DisassemblyLoader::getLabelTable(void) + { + std::vector fullTable; + for (auto& table : m_tables) + fullTable.insert(fullTable.end(), table.getLabelTable().begin(), table.getLabelTable().end()); + return fullTable; + } +} \ No newline at end of file diff --git a/src/debugger/DisassemblyLoader.hpp b/src/debugger/DisassemblyLoader.hpp new file mode 100644 index 0000000..a959020 --- /dev/null +++ b/src/debugger/DisassemblyLoader.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "../assembler/Assembler.hpp" + +#include + +namespace dragon +{ + class DisassemblyTable + { + public: + inline DisassemblyTable(void) { m_initialized = false; } + inline DisassemblyTable(const ostd::String& filePath) { init(filePath); } + void init(const ostd::String& filePath); + + inline const std::vector& getCodeTable(void) const { return m_code; } + inline const std::vector& getDataTable(void) const { return m_data; } + inline const std::vector& getLabelTable(void) const { return m_labels; } + + inline bool isInitialized(void) const { return m_initialized; } + + private: + void load_data(ostd::ByteStream& stream); + + private: + std::vector m_code; + std::vector m_labels; + std::vector m_data; + bool m_initialized { false }; + ostd::String m_filePath { "" }; + + public: + static const DisassemblyTable DefaultObject; + }; + + class DisassemblyLoader + { + public: + static void loadDirectory(const ostd::String& directoryPath); + static const DisassemblyTable& loadFile(const ostd::String& filePath); + + static std::vector getCodeTable(void); + static std::vector getDataTable(void); + static std::vector getLabelTable(void); + + private: + inline static std::vector m_tables; + }; +} \ No newline at end of file diff --git a/src/debugger/debugger_main.cpp b/src/debugger/debugger_main.cpp new file mode 100644 index 0000000..e942adf --- /dev/null +++ b/src/debugger/debugger_main.cpp @@ -0,0 +1,100 @@ +#include "Debugger.hpp" +#include "../runtime/DragonRuntime.hpp" + + +int main(int argc, char** argv) +{ + using namespace dragon; + int32_t rValue = Debugger::loadArguments(argc, argv); + if (rValue != 0) return rValue; + rValue = Debugger::initRuntime(); + if (rValue != 0) return rValue; + + if (!Debugger::data().args.auto_start_debug) + { + while (true) + { + Debugger::Display::printPrompt(); + Debugger::data().command = Debugger::getCommandInput(); + Debugger::data().command.trim().toLower(); + if (Debugger::data().command.str() == "r" || Debugger::data().command.str() == "run") + break; + else if (Debugger::data().command.str() == "q" || Debugger::data().command.str() == "quit") + return rValue; + else if (Debugger::data().command.startsWith("watch ")) + { + Debugger::data().command = Debugger::data().command.substr(6); + Debugger::data().command.trim(); + auto tokens = Debugger::data().command.tokenize(); + for (auto& addr : tokens) + { + Debugger::data().command = addr; + if (Debugger::data().command.isNumeric()) + Debugger::data().trackedAddresses.push_back((uint16_t)Debugger::data().command.toInt()); + else if (Debugger::data().command.startsWith("$")) + { + uint16_t addr = Debugger::Utils::findSymbol(Debugger::data().data, Debugger::data().command); + if (addr > 0) + Debugger::data().trackedAddresses.push_back(addr); + else + { + addr = Debugger::Utils::findSymbol(Debugger::data().labels, Debugger::data().command); + if (addr > 0) + Debugger::data().trackedAddresses.push_back(addr); + } + } + } + } + } + } + + constexpr int32_t labelLineLen = 20; + Debugger::data().currentAddress = dragon::DragonRuntime::cpu.readRegister(dragon::data::Registers::IP); + + bool userQuit = false; + while (true) + { + Debugger::data().command.clr(); + bool result = false; + bool hasError = false; + if (!userQuit) + { + result = dragon::DragonRuntime::runStep(Debugger::data().trackedAddresses); + hasError = dragon::DragonRuntime::hasError(); + Debugger::Display::printStep(); + Debugger::processErrors(); + } + if (!result || userQuit) + { + Debugger::output().nl().col(ostd::legacy::ConsoleCol::Yellow).p("Execution Finished. Pres to exit...").nl().reset(); + std::cin.get(); + break; + } + Debugger::Display::printPrompt(); + if (Debugger::data().args.step_exec || dragon::DragonRuntime::cpu.isInDebugBreakPoint()) + { + Debugger::data().command = Debugger::getCommandInput(); + if (dragon::DragonRuntime::cpu.isInDebugBreakPoint()) + Debugger::data().args.step_exec = true; + } + Debugger::data().command.trim().toLower(); + while (Debugger::data().command.str() != "") + { + if (Debugger::data().command.str() == "q" || Debugger::data().command.str() == "quit") + { + userQuit = true; + Debugger::data().command = ""; + } + else if (Debugger::data().command.str() == "c" || Debugger::data().command.str() == "continue") + { + Debugger::data().args.step_exec = false; + Debugger::data().command = ""; + } + else + Debugger::data().command = Debugger::Display::changeScreen(); + } + Debugger::data().currentAddress = dragon::DragonRuntime::cpu.readRegister(dragon::data::Registers::IP); + } + + return rValue; +} \ No newline at end of file diff --git a/src/gui/DebuggerWindow.cpp b/src/gui/DebuggerWindow.cpp new file mode 100644 index 0000000..792d429 --- /dev/null +++ b/src/gui/DebuggerWindow.cpp @@ -0,0 +1,6 @@ +#include "DebuggerWindow.hpp" + +namespace dragon +{ + +} \ No newline at end of file diff --git a/src/gui/DebuggerWindow.hpp b/src/gui/DebuggerWindow.hpp new file mode 100644 index 0000000..7666a76 --- /dev/null +++ b/src/gui/DebuggerWindow.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace dragon +{ + class DebuggerWindow + { + + }; +} // namespace dragon diff --git a/src/gui/RawTextRenderer.cpp b/src/gui/RawTextRenderer.cpp new file mode 100644 index 0000000..36d3aaf --- /dev/null +++ b/src/gui/RawTextRenderer.cpp @@ -0,0 +1,101 @@ +#include "RawTextRenderer.hpp" +#include +#include +#include + +namespace dragon +{ + void RawTextRenderer::initialize(void) + { + for (char c = ' '; c <= '~'; c++) + characterMap[c] = getCharacterIndex(c); + } + + bool RawTextRenderer::drawString(ostd::String str, uint32_t column, uint32_t row, uint32_t* screenPixels, int32_t screenWidth, int32_t screenHeight, uint32_t* fontPixels, ostd::Color color, ostd::Color background) + { + ostd::StringEditor se(str); + if (se.str() == "") return false; + if (row >= CONSOLE_CHARS_V) return false; + if (column >= CONSOLE_CHARS_H) return false; + if (column + str.length() > CONSOLE_CHARS_H) return false; + int32_t x = column * FONT_CHAR_W; + int32_t y = row * FONT_CHAR_H; + for (auto& c : str) + { + drawCharacter((uint8_t*)screenPixels, screenWidth, screenHeight, (uint8_t*)fontPixels, x, y, c, color, background); + x += FONT_CHAR_W; + } + return true; + } + + int32_t RawTextRenderer::getCharacterIndex(char c) + { + using namespace ostd; + + int32_t charIndex = (int)c - 32; + IPoint charCoords = CONVERT_1D_2D(charIndex, FONT_H_CHARS); + charCoords.x *= FONT_CHAR_W * 4; + charCoords.y *= FONT_CHAR_H; + charIndex = CONVERT_2D_1D(charCoords.x, charCoords.y, (FONT_H_CHARS * FONT_CHAR_W * 4)); + + return charIndex; + } + + ostd::Color RawTextRenderer::applyTint(ostd::Color baseColor, ostd::Color tintColor) + { + auto nBase = baseColor.getNormalizedColor(); + auto nTint = tintColor.getNormalizedColor(); + + float r = nBase.r * nTint.r; + float g = nBase.r * nTint.g; + float b = nBase.r * nTint.b; + + ostd::Color::FloatCol nTinted(r, g, b, 1.0f); + + return ostd::Color(nTinted); + } + + void RawTextRenderer::drawCharacter(uint8_t* screenPixels, int32_t screenWidth, int32_t screenHeight, uint8_t* fontPixels, int32_t x, int32_t y, char c, ostd::Color color, ostd::Color background) + { + using namespace ostd; + int32_t charIndex = characterMap[c]; + IPoint charCoords = CONVERT_1D_2D(charIndex, (FONT_CHAR_W * FONT_H_CHARS * 4)); + + int32_t screenx = x * 4, screeny = y; + + ostd::Color tintedColor; + + bool applyBackground = false; + for (int32_t y = charCoords.y; y < charCoords.y + (FONT_CHAR_H); y += 1) + { + for (int32_t x = charCoords.x; x < charCoords.x + (FONT_CHAR_W * 4); x += 4) + { + int32_t index = CONVERT_2D_1D(x, y, (FONT_CHAR_W * FONT_H_CHARS * 4)); + int32_t screenIndex = CONVERT_2D_1D(screenx, screeny, (screenWidth * 4)); + screenx += 4; + if (fontPixels[index] == 0x00 && fontPixels[index + 1] == 0x00 && fontPixels[index + 2] == 0x00) + { + if (background.a == 0) + continue; + applyBackground = true; + } + if (applyBackground) + { + screenPixels[screenIndex] = 255; + screenPixels[screenIndex + 1] = background.b; + screenPixels[screenIndex + 2] = background.g; + screenPixels[screenIndex + 3] = background.r; + applyBackground = false; + continue; + } + tintedColor = applyTint({ fontPixels[index], fontPixels[index + 1], fontPixels[index + 2], 255 }, color); + screenPixels[screenIndex] = fontPixels[index + 3]; + screenPixels[screenIndex + 1] = tintedColor.b; + screenPixels[screenIndex + 2] = tintedColor.g; + screenPixels[screenIndex + 3] = tintedColor.r; + } + screeny += 1; + screenx = x * 4; + } + } +} \ No newline at end of file diff --git a/src/gui/RawTextRenderer.hpp b/src/gui/RawTextRenderer.hpp new file mode 100644 index 0000000..9b0fc7d --- /dev/null +++ b/src/gui/RawTextRenderer.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace dragon +{ + class RawTextRenderer + { + private: + inline static std::unordered_map characterMap; + + public: + static void initialize(void); + static bool drawString(ostd::String str, uint32_t column, uint32_t row, uint32_t* screenPixels, int32_t screenWidth, int32_t screenHeight, uint32_t* fontPixels, ostd::Color color = { 255, 255, 255, 255 }, ostd::Color background = { 255, 255, 255, 0 }); + + private: + static int32_t getCharacterIndex(char c); + static ostd::Color applyTint(ostd::Color baseColor, ostd::Color tintColor); + static void drawCharacter(uint8_t* screenPixels, int32_t screenWidth, int32_t screenHeight, uint8_t* fontPixels, int32_t x, int32_t y, char c, ostd::Color color = { 255, 255, 255, 255 }, ostd::Color background = { 255, 255, 255, 0 }); + + public: + inline static constexpr int32_t FONT_CHAR_W = 11; + inline static constexpr int32_t FONT_CHAR_H = 26; + inline static constexpr int32_t FONT_V_CHARS = 6; + inline static constexpr int32_t FONT_H_CHARS = 16; + inline static constexpr int32_t CONSOLE_CHARS_H = 90; + inline static constexpr int32_t CONSOLE_CHARS_V = 21; + }; +} \ No newline at end of file diff --git a/src/gui/VirtualConsole.cpp b/src/gui/VirtualConsole.cpp new file mode 100644 index 0000000..e2db9b9 --- /dev/null +++ b/src/gui/VirtualConsole.cpp @@ -0,0 +1,158 @@ +#include "VirtualConsole.hpp" +#include "RawTextRenderer.hpp" +#include + +#include "../hardware/VirtualIODevices.hpp" +#include "../hardware/VirtualCPU.hpp" + +namespace dragon +{ + void VirtualConsole::initialize(void) + { + m_windowWidth = RawTextRenderer::CONSOLE_CHARS_H * RawTextRenderer::FONT_CHAR_W; //60 * 16; + m_windowHeight = RawTextRenderer::CONSOLE_CHARS_V * RawTextRenderer::FONT_CHAR_H; //60 * 9; + + SDL_Init(SDL_INIT_VIDEO); + m_window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, m_windowWidth, m_windowHeight, SDL_WINDOW_RESIZABLE); + SDL_SetWindowResizable(m_window, SDL_FALSE); + m_renderer = SDL_CreateRenderer(m_window, -1, SDL_RENDERER_ACCELERATED); + SDL_SetWindowMinimumSize(m_window, m_windowWidth, m_windowHeight); + m_screenTexture = SDL_CreateTexture(m_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, m_windowWidth, m_windowHeight); + m_fontSurface = SDL_LoadBMP("font.bmp"); + if (m_fontSurface == NULL) + out.bg(ostd::ConsoleColors::Red).p("Error loading font.").nl(); + m_fontPixels = (uint32_t*)m_fontSurface->pixels; + m_screenPixels = (uint32_t*)malloc(m_windowWidth * m_windowHeight * 4); + + dragon::RawTextRenderer::initialize(); + + vout.init(m_screenPixels, m_windowWidth, m_windowHeight, m_fontPixels); + vout.enableFixedScreen(); + + m_palettes.push_back(new data::BiosVideoDefaultPalette); //TODO: Possible Memory Leak, palettes are never destroyed + + m_initialized = true; + m_running = true; + } + + void VirtualConsole::update(void) + { + if (!m_initialized) return; + Uint64 start = SDL_GetPerformanceCounter(); + handleEvents(); + if (m_redrawConsole) + { + uint8_t signal = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::Signal); + switch (signal) + { + case tSignals::ClearScreen: + m_clearMemory = true; + break; + case tSignals::Continue: break; + default: break; + } + m_biosVideo.write8(hw::VirtualBIOSVideo::tRegisters::Signal, tSignals::Continue); + drawConsole(); + m_redrawConsole = false; + } + finalizeRender(); + Uint64 end = SDL_GetPerformanceCounter(); + float elapsed = (end - start) / (float)SDL_GetPerformanceFrequency(); + m_redrawAccumulator += elapsed; + if (m_redrawAccumulator >= 0.2f) + { + m_redrawConsole = true; + m_redrawAccumulator = 0.0f; + } + end = SDL_GetPerformanceCounter(); + elapsed = (end - start) / (float)SDL_GetPerformanceFrequency(); + m_timeAccumulator += elapsed; + if (m_timeAccumulator >= 0.5f) + { + m_title.clr().add("FPS: ").addi((int)(1.0f / elapsed)); + SDL_SetWindowTitle(m_window, m_title.c_str()); + m_timeAccumulator = 0.0f; + } + } + + void VirtualConsole::handleEvents(void) + { + if (!m_initialized) return; + SDL_Event event; + while (SDL_PollEvent(&event)) + { + if (event.type == SDL_QUIT) exit(0); + } + } + + void VirtualConsole::finalizeRender(void) + { + SDL_RenderClear(m_renderer); + SDL_UpdateTexture(m_screenTexture, NULL, m_screenPixels, m_windowWidth * 4); + SDL_RenderCopy(m_renderer, m_screenTexture, NULL, NULL); + SDL_RenderPresent(m_renderer); + } + + void VirtualConsole::drawConsole(void) + { + const uint8_t TRANSPARENCY_COLOR_INDEX = 0x5; + + uint8_t clearColor = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::ClearColor); + uint8_t palette = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::Palette); + bool useTransparency = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::UseTransparencyOn0x5) != 0; + + if (palette >= m_palettes.size()) + palette = 0; //TODO: Error + + auto& pal = *m_palettes[palette]; + + ostd::Color clrCol = pal.getColor(clearColor); + if (m_clearMemory) + { + for (int y = 0; y < m_windowHeight; ++y) + { + for (int x = 0; x < m_windowWidth; ++x) + { + m_screenPixels[x + y * m_windowWidth] = clrCol.asInteger(); + } + } + m_clearMemory = false; + } + + ostd::Color fgCol; + ostd::Color bgCol; + uint16_t index = hw::VirtualBIOSVideo::tRegisters::VideoMemoryStart; + + // if (m_clearMemory) + // { + // for (int32_t i = index; i < (index + (RawTextRenderer::CONSOLE_CHARS_H * RawTextRenderer::CONSOLE_CHARS_V * 2)); i += 2) + // { + // m_biosVideo.write8((uint16_t)i, 0x00); + // m_biosVideo.write8((uint16_t)(i + 1), clearColor); + // } + // m_clearMemory = false; + // } + + for (int32_t i = index; i < (index + (RawTextRenderer::CONSOLE_CHARS_H * RawTextRenderer::CONSOLE_CHARS_V * 2)); i += 2) + { + uint8_t character = m_biosVideo.read8((uint16_t)i); + uint8_t colors = m_biosVideo.read8((uint16_t)(i + 1)); + uint8_t bgcol = (colors >> 4) & 0xF; + uint8_t fgcol = (colors & 0xF); + if (!isprint(character)) + { + character = ' '; + fgCol = pal.getColor(fgcol); + bgCol = clrCol; + } + else + { + fgCol = pal.getColor(fgcol); + bgCol = pal.getColor(bgcol); + } + vout.bgcol(bgCol).col(fgCol).p((char)character).flush().bgcol(clrCol); + } + vout.refreshScreen(); + m_cpu.handleInterrupt(data::InterruptCodes::BiosVideoScreenRefresh); + } +} \ No newline at end of file diff --git a/src/gui/VirtualConsole.hpp b/src/gui/VirtualConsole.hpp new file mode 100644 index 0000000..0ae5b03 --- /dev/null +++ b/src/gui/VirtualConsole.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "../tools/SDLInclude.hpp" + +#include +#include "VirtualConsoleOutputHandler.hpp" +#include "../tools/GlobalData.hpp" + +namespace dragon +{ + namespace hw + { + class VirtualBIOSVideo; + class VirtualCPU; + } + class VirtualConsole + { + public: struct tSignals + { + inline static constexpr uint8_t Continue = 0x00; + inline static constexpr uint8_t ClearScreen = 0x01; + }; + + public: + inline VirtualConsole(hw::VirtualBIOSVideo& biosVideo, hw::VirtualCPU& cpu) : m_biosVideo(biosVideo), m_cpu(cpu) { } + inline bool isInitialized(void) const { return m_initialized; } + inline bool isRunning(void) const { return m_running; } + void initialize(void); + void update(void); + + private: + void handleEvents(void); + void finalizeRender(void); + void drawConsole(void); + + private: + ostd::ConsoleOutputHandler out; + dragon::VirtualConsoleOutputHandler vout; + + SDL_Window* m_window { nullptr }; + SDL_Renderer* m_renderer { nullptr }; + SDL_Texture* m_screenTexture { nullptr }; + SDL_Surface* m_fontSurface { nullptr }; + + int32_t m_windowWidth { 0 }; + int32_t m_windowHeight { 0 }; + + uint32_t* m_fontPixels { nullptr }; + uint32_t* m_screenPixels { nullptr }; + + bool m_redrawConsole { true }; + float m_timeAccumulator { 0.0f }; + bool m_running { false }; + + ostd::StringEditor m_title { "" }; + float m_redrawAccumulator { 0.0f }; + bool m_initialized { false }; + bool m_clearMemory { false }; + + hw::VirtualBIOSVideo& m_biosVideo; + hw::VirtualCPU& m_cpu; + std::vector m_palettes; + }; +} \ No newline at end of file diff --git a/src/gui/VirtualConsoleOutputHandler.cpp b/src/gui/VirtualConsoleOutputHandler.cpp new file mode 100644 index 0000000..5e738b6 --- /dev/null +++ b/src/gui/VirtualConsoleOutputHandler.cpp @@ -0,0 +1,303 @@ +#include "VirtualConsoleOutputHandler.hpp" +#include "RawTextRenderer.hpp" + +#include +#include + +namespace dragon +{ + tRichChar RichString::at(uint32_t index) const + { + if (index >= m_text.size()) + { + OX_WARN("ox::RichString::at(...): Index out of bounds."); + return tRichChar(); + } + return { (unsigned char)m_text[index], m_foreground[index], m_background[index] }; + } + + void RichString::add(tRichChar rchar) + { + m_text += rchar.ascii; + m_foreground.push_back(rchar.foreground); + m_background.push_back(rchar.background); + } + + void RichString::add(ostd::String str, ostd::Color fg, ostd::Color bg) + { + for (auto& c : str) + { + m_text += c; + m_foreground.push_back(fg); + m_background.push_back(bg); + } + } + + void RichString::add(ostd::String str) + { + ostd::Color fcol(255); + ostd::Color bcol(0, 0); + if (m_text.length() > 0) + { + fcol = m_foreground[m_foreground.size() - 1]; + bcol = m_background[m_background.size() - 1]; + } + add(str, fcol, bcol); + } + + void RichString::clear(void) + { + m_text = ""; + m_background.clear(); + m_foreground.clear(); + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::col(ostd::String color) + { + //TODO: Maybe implement?? + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::col(const ostd::Color& color) + { + m_currentForegroundColor = color; + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::p(char c) + { + check_if_new_line(); + m_currentBufferEditor.add(c); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::p(const ostd::StringEditor& se) + { + check_if_new_line(); + m_currentBufferEditor.add(se.str()); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(uint8_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(int8_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(uint16_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(int16_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(uint32_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(int32_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(uint64_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pi(int64_t i) + { + check_if_new_line(); + m_currentBufferEditor.addi(i); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pf(float f, uint8_t precision) + { + //TODO: Implement precision + check_if_new_line(); + m_currentBufferEditor.addf(f); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::pf(double f, uint8_t precision) + { + //TODO: Implement precision + check_if_new_line(); + m_currentBufferEditor.addf(f); + add_current_to_rich_buffer(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::nl(void) + { + m_newLine = true; + flush(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::flush(void) + { + if (!isInitialized()) return *this; + if (m_currentBuffer.getText() == "") return *this; + + m_lines.push_back(m_currentBuffer); + m_currentBuffer.clear(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::reset(void) + { + m_currentBuffer.clear(); + m_currentBufferEditor.clr(); + m_lines.clear(); + m_newLine = true; + resetColors(); + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::clear(void) + { + //TODO: Implement + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::bgcol(const ostd::Color& color) + { + m_currentBackgroundColor = color; + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::bgcol(ostd::String color) + { + //TODO: Maybe implement + return *this; + } + + ostd::legacy::IOutputHandler& VirtualConsoleOutputHandler::resetColors(void) + { + m_currentForegroundColor = m_defaultForegroundColor; + m_currentBackgroundColor = m_defaultBackgroundColor; + return *this; + } + + void VirtualConsoleOutputHandler::init(uint32_t* screenPixels, int32_t screenWidth, int32_t screenHeight, uint32_t* fontPixels) + { + m_screenPixels = screenPixels; + m_fontPixels = fontPixels; + m_screenHeight = screenHeight; + m_screenWidth = screenWidth; + + m_initialized = m_screenHeight > 0 && m_screenWidth > 0 && m_screenPixels != nullptr && m_fontPixels != nullptr; + } + + void VirtualConsoleOutputHandler::add_current_to_rich_buffer(void) + { + if (m_currentBuffer.getText().length() + m_currentBufferEditor.len() <= RawTextRenderer::CONSOLE_CHARS_H) + { + m_currentBuffer.add(m_currentBufferEditor.str(), m_currentForegroundColor, m_currentBackgroundColor); + m_currentBufferEditor.clr(); + if (m_currentBuffer.getText().length() == RawTextRenderer::CONSOLE_CHARS_H) + nl(); + else + m_newLine = false; + return; + } + int32_t extraStringLength = (m_currentBuffer.getText().length() + m_currentBufferEditor.len()) - RawTextRenderer::CONSOLE_CHARS_H; + ostd::String excess = m_currentBufferEditor.substr(RawTextRenderer::CONSOLE_CHARS_H); + ostd::String end = m_currentBufferEditor.substr(0, RawTextRenderer::CONSOLE_CHARS_H); + m_currentBuffer.add(end, m_currentForegroundColor, m_currentBackgroundColor); + nl(); + m_currentBufferEditor = excess; + add_current_to_rich_buffer(); + } + + void VirtualConsoleOutputHandler::check_if_new_line(void) + { + if (m_newLine) return; + if (m_lines.size() > 0) + { + m_currentBuffer = m_lines[m_lines.size() - 1]; + m_lines.pop_back(); + } + + } + + void VirtualConsoleOutputHandler::draw_rich_String(const RichString& rstr, uint32_t row) + { + for (int32_t i = 0; i < rstr.getText().length(); i++) + { + auto rchar = rstr.at(i); + RawTextRenderer::drawString(ostd::StringEditor().add(rchar.ascii).str(), i, row, m_screenPixels, m_screenWidth, m_screenHeight, m_fontPixels, rchar.foreground, rchar.background); + } + } + + void VirtualConsoleOutputHandler::refreshScreen(void) + { + uint32_t row = 0; + if (m_lines.size() > RawTextRenderer::CONSOLE_CHARS_V - 1) + { + if (m_fixedScreen) + { + for (int32_t i = 0; i < RawTextRenderer::CONSOLE_CHARS_V; i++) + { + draw_rich_String(m_lines[i], row); + row++; + } + } + else + { + for (int32_t i = m_lines.size() - RawTextRenderer::CONSOLE_CHARS_V + 1; i < m_lines.size(); i++) + { + draw_rich_String(m_lines[i], row); + row++; + } + } + } + else + { + for (auto& line : m_lines) + { + draw_rich_String(line, row); + row++; + } + } + + m_lines.clear(); + m_currentBuffer.clear(); + m_currentBufferEditor.clr(); + } +} \ No newline at end of file diff --git a/src/gui/VirtualConsoleOutputHandler.hpp b/src/gui/VirtualConsoleOutputHandler.hpp new file mode 100644 index 0000000..666b114 --- /dev/null +++ b/src/gui/VirtualConsoleOutputHandler.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include + +#include + +#include + +namespace dragon +{ + struct tRichChar + { + unsigned char ascii { 0 }; + ostd::Color foreground { 0, 0 }; + ostd::Color background { 0, 0 }; + }; + + class RichString //TODO: Legacy RichString should be replaced with new tStyledString + { + public: + inline RichString(void) { } + inline ostd::String getText(void) const { return m_text; } + tRichChar at(uint32_t index) const; + void add(tRichChar rchar); + void add(ostd::String str, ostd::Color fg, ostd::Color bg); + void add(ostd::String str); + void clear(void); + + private: + ostd::String m_text { "" }; + std::vector m_foreground; + std::vector m_background; + }; + + class VirtualConsoleOutputHandler : public ostd::legacy::IOutputHandler + { + public: + ostd::legacy::IOutputHandler& col(ostd::String color) override; + ostd::legacy::IOutputHandler& col(const ostd::Color& color) override; + ostd::legacy::IOutputHandler& p(char c) override; + ostd::legacy::IOutputHandler& p(const ostd::StringEditor& se) override; + ostd::legacy::IOutputHandler& pi(uint8_t i) override; + ostd::legacy::IOutputHandler& pi(int8_t i) override; + ostd::legacy::IOutputHandler& pi(uint16_t i) override; + ostd::legacy::IOutputHandler& pi(int16_t i) override; + ostd::legacy::IOutputHandler& pi(uint32_t i) override; + ostd::legacy::IOutputHandler& pi(int32_t i) override; + ostd::legacy::IOutputHandler& pi(uint64_t i) override; + ostd::legacy::IOutputHandler& pi(int64_t i) override; + ostd::legacy::IOutputHandler& pf(float f, uint8_t precision = 0) override; + ostd::legacy::IOutputHandler& pf(double f, uint8_t precision = 0) override; + ostd::legacy::IOutputHandler& nl(void) override; + inline ostd::legacy::IOutputHandler& pStyled(const ostd::StringEditor& styled) override { return *this; } //TODO: Implement + inline ostd::legacy::IOutputHandler& pStyled(const ostd::TextStyleBuilder::IRichStringBase& styled) override { return *this; } //TODO: Implement + inline ostd::legacy::IOutputHandler& pStyled(const ostd::TextStyleParser::tStyledString& styled) override { return *this; }; //TODO: Implement + ostd::legacy::IOutputHandler& flush(void) override; + ostd::legacy::IOutputHandler& reset(void) override; + ostd::legacy::IOutputHandler& clear(void) override; + + ostd::legacy::IOutputHandler& bgcol(const ostd::Color& color) override; + ostd::legacy::IOutputHandler& bgcol(ostd::String color) override; + ostd::legacy::IOutputHandler& resetColors(void) override; + + void init(uint32_t* screenPixels, int32_t screenWidth, int32_t screenHeight, uint32_t* fontPixels); + inline bool isInitialized(void) const { return m_initialized; } + inline bool isFixedScreen(void) const { return m_fixedScreen; } + inline void enableFixedScreen(bool fs = true) { m_fixedScreen = fs; } + + void refreshScreen(void); + + private: + void add_current_to_rich_buffer(void); + void check_if_new_line(void); + void draw_rich_String(const RichString& rstr, uint32_t row); + + private: + std::vector m_lines; + RichString m_currentBuffer; + ostd::StringEditor m_currentBufferEditor; + + // std::vector m_allLines; + // ostd::StringEditor m_buffer; + // int32_t m_currentRow { 0 }; + // int32_t m_currentColumn { 0 }; + ostd::Color m_currentForegroundColor { 255, 255, 255, 255 }; + ostd::Color m_currentBackgroundColor { 255, 255, 255, 0 }; + ostd::Color m_defaultForegroundColor { 255, 255, 255, 255 }; + ostd::Color m_defaultBackgroundColor { 255, 255, 255, 0 }; + + uint32_t* m_screenPixels { nullptr }; + uint32_t* m_fontPixels { nullptr }; + int32_t m_screenWidth { 0 }; + int32_t m_screenHeight { 0 }; + bool m_initialized { false }; + bool m_newLine { true }; + bool m_fixedScreen { false }; + }; +} \ No newline at end of file diff --git a/src/gui/Window.cpp b/src/gui/Window.cpp new file mode 100644 index 0000000..dd624e9 --- /dev/null +++ b/src/gui/Window.cpp @@ -0,0 +1,98 @@ +#include "Window.hpp" + +namespace dragon +{ + void Window::initialize(int32_t width, int32_t height, const ostd::String& fontPath) + { + m_windowWidth = width; + m_windowHeight = height; + + SDL_Init(SDL_INIT_VIDEO); + m_window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, m_windowWidth, m_windowHeight, SDL_WINDOW_RESIZABLE); + SDL_SetWindowResizable(m_window, SDL_FALSE); + m_renderer = SDL_CreateRenderer(m_window, -1, SDL_RENDERER_ACCELERATED); + SDL_SetWindowMinimumSize(m_window, m_windowWidth, m_windowHeight); + // m_screenTexture = SDL_CreateTexture(m_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, m_windowWidth, m_windowHeight); + m_fontSurface = SDL_LoadBMP(fontPath.c_str()); + if (m_fontSurface == NULL) + out.bg(ostd::ConsoleColors::Red).p("Error loading font.").reset().nl(); + m_fontPixels = (uint32_t*)m_fontSurface->pixels; + // m_screenPixels = (uint32_t*)malloc(m_windowWidth * m_windowHeight * 4); + + m_initialized = true; + m_running = true; + } + + void Window::update(void) + { + if (!m_initialized) return; + Uint64 start = SDL_GetPerformanceCounter(); + handleEvents(); + SDL_RenderClear(m_renderer); + for (int32_t i = 0; i < m_widgets.size(); i++) + { + if (m_widgets[i] == nullptr) continue; + m_widgets[i]->draw(); + } + SDL_RenderPresent(m_renderer); + // finalizeRender(); + Uint64 end = SDL_GetPerformanceCounter(); + float elapsed = (end - start) / (float)SDL_GetPerformanceFrequency(); + m_redrawAccumulator += elapsed; + if (m_redrawAccumulator >= 0.2f) + { + for (int32_t i = 0; i < m_widgets.size(); i++) + { + if (m_widgets[i] == nullptr) continue; + m_widgets[i]->fixedUpdate(); + } + m_redrawAccumulator = 0.0f; + } + end = SDL_GetPerformanceCounter(); + elapsed = (end - start) / (float)SDL_GetPerformanceFrequency(); + m_timeAccumulator += elapsed; + if (m_timeAccumulator >= 0.5f) + { + for (int32_t i = 0; i < m_widgets.size(); i++) + { + if (m_widgets[i] == nullptr) continue; + m_widgets[i]->slowUpdate(); + } + m_title.clr().add("FPS: ").addi((int)(1.0f / elapsed)); + SDL_SetWindowTitle(m_window, m_title.c_str()); + m_timeAccumulator = 0.0f; + } + } + + bool Window::addWidget(Widget& widget) + { + widget.__init(*this); + m_widgets.push_back(&widget); + return true; + } + + void Window::setSize(int32_t width, int32_t height) + { + if (!isInitialized()) return; + SDL_SetWindowSize(m_window, width, height); + } + + void Window::handleEvents(void) + { + if (!m_initialized) return; + SDL_Event event; + while (SDL_PollEvent(&event)) + { + if (event.type == SDL_QUIT) + { + m_running = false; + } + } + } + + void Window::finalizeRender(void) + { + // SDL_UpdateTexture(m_screenTexture, NULL, m_screenPixels, m_windowWidth * 4); + // SDL_RenderCopy(m_renderer, m_screenTexture, NULL, NULL); + } +} \ No newline at end of file diff --git a/src/gui/Window.hpp b/src/gui/Window.hpp new file mode 100644 index 0000000..2823d5f --- /dev/null +++ b/src/gui/Window.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "../tools/SDLInclude.hpp" +#include +#include +#include "VirtualConsoleOutputHandler.hpp" +#include "../tools/GlobalData.hpp" +#include "../gui/widgets/Widget.hpp" + +namespace dragon +{ + class Window + { + public: + inline Window(void) { } + inline bool isInitialized(void) const { return m_initialized; } + inline bool isRunning(void) const { return m_running; } + inline void hide(void) { SDL_HideWindow(m_window); } + inline void show(void) { SDL_ShowWindow(m_window); } + void initialize(int32_t width, int32_t height, const ostd::String& fontPath); + void update(void); + bool addWidget(Widget& widget); + void setSize(int32_t width, int32_t height); + + private: + void handleEvents(void); + void finalizeRender(void); + + private: + ostd::ConsoleOutputHandler out; + + std::vector m_widgets; + + SDL_Window* m_window { nullptr }; + SDL_Renderer* m_renderer { nullptr }; + // SDL_Texture* m_screenTexture { nullptr }; + SDL_Surface* m_fontSurface { nullptr }; + + int32_t m_windowWidth { 0 }; + int32_t m_windowHeight { 0 }; + + uint32_t* m_fontPixels { nullptr }; + // uint32_t* m_screenPixels { nullptr }; + + float m_timeAccumulator { 0.0f }; + bool m_running { false }; + + ostd::StringEditor m_title { "" }; + float m_redrawAccumulator { 0.0f }; + bool m_initialized { false }; + + //Signals + inline static const uint32_t Signal_OnMousePressed = ostd::SignalHandler::newCustomSignal(4096); + + friend class VirtualConsoleWidtget; + }; +} \ No newline at end of file diff --git a/src/gui/widgets/VirtualConsoleWidget.cpp b/src/gui/widgets/VirtualConsoleWidget.cpp new file mode 100644 index 0000000..6445c96 --- /dev/null +++ b/src/gui/widgets/VirtualConsoleWidget.cpp @@ -0,0 +1,128 @@ +#include "VirtualConsoleWidget.hpp" +#include "../../gui/RawTextRenderer.hpp" +#include "../../gui/Window.hpp" +#include "../../hardware/VirtualIODevices.hpp" +#include "../../hardware/VirtualCPU.hpp" + +namespace dragon +{ + void VirtualConsoleWidtget::init(void) + { + setw(RawTextRenderer::CONSOLE_CHARS_H * RawTextRenderer::FONT_CHAR_W); //60 * 16; + seth(RawTextRenderer::CONSOLE_CHARS_V * RawTextRenderer::FONT_CHAR_H); //60 * 9; + setPosition(0, 0); + + dragon::RawTextRenderer::initialize(); + + m_pixels = new uint32_t[getw() * geth()]; //TODO: Delete (Memory Leak) + + vout.init(m_pixels, getw(), geth(), m_parent->m_fontPixels); + vout.enableFixedScreen(); + + m_palettes.push_back(new data::BiosVideoDefaultPalette); //TODO: Possible Memory Leak, palettes are never destroyed + + m_texture = SDL_CreateTexture(m_parent->m_renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, getw(), geth()); + + m_initialized = true; + validate(); + } + + void VirtualConsoleWidtget::draw(void) + { + if (isInvalid()) return; + SDL_Rect rect { (int)getx(), (int)gety(), (int)getw(), (int)geth() }; + SDL_RenderCopy(m_parent->m_renderer, m_texture, NULL, &rect); + } + + void VirtualConsoleWidtget::drawConsole(void) + { + if (isInvalid()) return; + const uint8_t TRANSPARENCY_COLOR_INDEX = 0x5; + + uint8_t clearColor = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::ClearColor); + uint8_t palette = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::Palette); + bool useTransparency = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::UseTransparencyOn0x5) != 0; + + if (palette >= m_palettes.size()) + palette = 0; //TODO: Error + + auto& pal = *m_palettes[palette]; + + ostd::Color clrCol = pal.getColor(clearColor); + if (m_clearMemory) + { + for (int y = 0; y < m_parent->m_windowHeight; ++y) + { + for (int x = 0; x < m_parent->m_windowWidth; ++x) + { + m_pixels[x + y * m_parent->m_windowWidth] = clrCol.asInteger(); + } + } + m_clearMemory = false; + } + + ostd::Color fgCol; + ostd::Color bgCol; + uint16_t index = hw::VirtualBIOSVideo::tRegisters::VideoMemoryStart; + + // if (m_clearMemory) + // { + // for (int32_t i = index; i < (index + (RawTextRenderer::CONSOLE_CHARS_H * RawTextRenderer::CONSOLE_CHARS_V * 2)); i += 2) + // { + // m_biosVideo.write8((uint16_t)i, 0x00); + // m_biosVideo.write8((uint16_t)(i + 1), clearColor); + // } + // m_clearMemory = false; + // } + + for (int32_t i = index; i < (index + (RawTextRenderer::CONSOLE_CHARS_H * RawTextRenderer::CONSOLE_CHARS_V * 2)); i += 2) + { + uint8_t character = m_biosVideo.read8((uint16_t)i); + uint8_t colors = m_biosVideo.read8((uint16_t)(i + 1)); + uint8_t bgcol = (colors >> 4) & 0xF; + uint8_t fgcol = (colors & 0xF); + if (!isprint(character)) + { + character = ' '; + fgCol = pal.getColor(fgcol); + bgCol = clrCol; + } + else + { + fgCol = pal.getColor(fgcol); + bgCol = pal.getColor(bgcol); + } + vout.bgcol(bgCol).col(fgCol).p((char)character).flush().bgcol(clrCol); + } + vout.refreshScreen(); + m_cpu.handleInterrupt(data::InterruptCodes::BiosVideoScreenRefresh); + } + + void VirtualConsoleWidtget::update(void) + { + + } + + void VirtualConsoleWidtget::fixedUpdate(void) + { + if (isInvalid()) return; + uint8_t signal = m_biosVideo.read8(hw::VirtualBIOSVideo::tRegisters::Signal); + switch (signal) + { + case tSignals::ClearScreen: + m_clearMemory = true; + break; + case tSignals::Continue: break; + default: break; + } + m_biosVideo.write8(hw::VirtualBIOSVideo::tRegisters::Signal, tSignals::Continue); + drawConsole(); + SDL_UpdateTexture(m_texture, NULL, m_pixels, getw() * 4); + } + + void VirtualConsoleWidtget::slowUpdate(void) + { + + } + +} \ No newline at end of file diff --git a/src/gui/widgets/VirtualConsoleWidget.hpp b/src/gui/widgets/VirtualConsoleWidget.hpp new file mode 100644 index 0000000..8258e51 --- /dev/null +++ b/src/gui/widgets/VirtualConsoleWidget.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "Widget.hpp" + +#include "../../tools/SDLInclude.hpp" + +#include "../../gui/VirtualConsoleOutputHandler.hpp" +#include "../../tools/GlobalData.hpp" + +namespace dragon +{ + namespace hw + { + class VirtualBIOSVideo; + class VirtualCPU; + } + class VirtualConsoleWidtget : public Widget + { + public: struct tSignals + { + inline static constexpr uint8_t Continue = 0x00; + inline static constexpr uint8_t ClearScreen = 0x01; + }; + public: + inline VirtualConsoleWidtget(hw::VirtualBIOSVideo& biosVideo, hw::VirtualCPU& cpu) : m_biosVideo(biosVideo), m_cpu(cpu) { invalidate(); } + void init(void) override; + void draw(void) override; + void update(void) override; + void fixedUpdate(void) override; + void slowUpdate(void) override; + + inline bool isInitialized(void) const { return m_initialized; } + + private: + void drawConsole(void); + + private: + dragon::VirtualConsoleOutputHandler vout; + + // SDL_Window* m_window { nullptr }; + // SDL_Renderer* m_renderer { nullptr }; + // SDL_Texture* m_screenTexture { nullptr }; + // SDL_Surface* m_fontSurface { nullptr }; + + // int32_t m_windowWidth { 0 }; + // int32_t m_windowHeight { 0 }; + + // uint32_t* m_fontPixels { nullptr }; + // uint32_t* m_screenPixels { nullptr }; + + uint32_t* m_pixels { nullptr }; + SDL_Texture* m_texture; + + // bool m_redrawConsole { true }; + // float m_timeAccumulator { 0.0f }; + // bool m_running { false }; + + // ostd::StringEditor m_title { "" }; + // float m_redrawAccumulator { 0.0f }; + bool m_initialized { false }; + bool m_clearMemory { false }; + + hw::VirtualBIOSVideo& m_biosVideo; + hw::VirtualCPU& m_cpu; + std::vector m_palettes; + }; +} \ No newline at end of file diff --git a/src/gui/widgets/Widget.hpp b/src/gui/widgets/Widget.hpp new file mode 100644 index 0000000..cdcc23b --- /dev/null +++ b/src/gui/widgets/Widget.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +namespace dragon +{ + class Window; + class Widget : public ostd::Rectangle, public ostd::BaseObject + { + public: + void __init(Window& parent) { m_parent = &parent; init(); } + virtual void draw(void) { } + virtual void update(void) { } + virtual void fixedUpdate(void) { } + virtual void slowUpdate(void) { } + + protected: + virtual void init(void) { } + + protected: + Window* m_parent { nullptr }; + }; +} \ No newline at end of file diff --git a/src/hardware/IMemoryDevice.hpp b/src/hardware/IMemoryDevice.hpp new file mode 100644 index 0000000..47d1a77 --- /dev/null +++ b/src/hardware/IMemoryDevice.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace dragon +{ + namespace hw + { + class IMemoryDevice + { + public: + virtual int8_t read8(uint16_t addr) = 0; + virtual int16_t read16(uint16_t addr) = 0; + virtual int8_t write8(uint16_t addr, int8_t value) = 0; + virtual int16_t write16(uint16_t addr, int16_t value) = 0; + virtual inline ostd::ByteStream* getByteStream(void) = 0; + }; + } +} \ No newline at end of file diff --git a/src/hardware/MemoryMapper.cpp b/src/hardware/MemoryMapper.cpp new file mode 100644 index 0000000..518d8bd --- /dev/null +++ b/src/hardware/MemoryMapper.cpp @@ -0,0 +1,75 @@ +#include "MemoryMapper.hpp" +#include +#include + +#include "../tools/GlobalData.hpp" + +namespace dragon +{ + namespace hw + { + MemoryMapper::MemoryMapper(void) + { + } + + int8_t MemoryMapper::read8(uint16_t addr) + { + auto region = findRegion(addr); + if (region == nullptr) return 0x0000; //TODO: Error + uint16_t finalAddr = (region->remap ? addr - region->startAddress : addr); + return region->device->read8(finalAddr); + } + + int16_t MemoryMapper::read16(uint16_t addr) + { + auto region = findRegion(addr); + if (region == nullptr) return 0x0000; //TODO: Error + uint16_t finalAddr = (region->remap ? addr - region->startAddress : addr); + return region->device->read16(finalAddr); + } + + int8_t MemoryMapper::write8(uint16_t addr, int8_t value) + { + auto region = findRegion(addr); + if (region == nullptr) return 0x0000; //TODO: Error + uint16_t finalAddr = (region->remap ? addr - region->startAddress : addr); + return region->device->write8(finalAddr, value); + } + + int16_t MemoryMapper::write16(uint16_t addr, int16_t value) + { + auto region = findRegion(addr); + if (region == nullptr) return 0x0000; //TODO: Error + uint16_t finalAddr = (region->remap ? addr - region->startAddress : addr); + return region->device->write16(finalAddr, value); + } + + void MemoryMapper::mapDevice(IMemoryDevice& device, uint16_t startAddr, uint16_t endAddr, bool remap, ostd::String name) + { + m_regions.push_back({ &device, startAddr, endAddr, remap, name }); + } + + ostd::String MemoryMapper::getMemoryRegionName(uint16_t address) + { + tMemoryRegion* region = findRegion(address); + if (region == nullptr) return "Invalid"; + return region->name; + } + + ostd::ByteStream* MemoryMapper::getByteStream(void) + { + return nullptr; + } + + MemoryMapper::tMemoryRegion* MemoryMapper::findRegion(uint16_t address) + { + for (auto& region : m_regions) + { + if (address >= region.startAddress && address <= region.endAddress) + return ®ion; + } + data::ErrorHandler::pushError(data::ErrorCodes::MM_RegionNotFound, ostd::StringEditor("Memory device not found for address: ").add(ostd::Utils::getHexStr(address, true, 2)).str()); + return nullptr; //TODO: Error + } + } +} \ No newline at end of file diff --git a/src/hardware/MemoryMapper.hpp b/src/hardware/MemoryMapper.hpp new file mode 100644 index 0000000..525deec --- /dev/null +++ b/src/hardware/MemoryMapper.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "IMemoryDevice.hpp" +#include + +namespace dragon +{ + namespace hw + { + class MemoryMapper : public IMemoryDevice + { + private: struct tMemoryRegion + { + IMemoryDevice* device { nullptr }; + uint16_t startAddress { 0x0000 }; + uint16_t endAddress { 0x0000 }; + bool remap { false }; + ostd::String name { "" }; + }; + + public: + MemoryMapper(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + void mapDevice(IMemoryDevice& device, uint16_t startAddr, uint16_t endAddr, bool remap = false, ostd::String name = ""); + + ostd::String getMemoryRegionName(uint16_t address); + + ostd::ByteStream* getByteStream(void) override; + + private: + tMemoryRegion* findRegion(uint16_t address); + + private: + std::vector m_regions; + }; + } +} \ No newline at end of file diff --git a/src/hardware/VirtualCPU.cpp b/src/hardware/VirtualCPU.cpp new file mode 100644 index 0000000..8a98026 --- /dev/null +++ b/src/hardware/VirtualCPU.cpp @@ -0,0 +1,707 @@ +#include "VirtualCPU.hpp" +#include "../tools/GlobalData.hpp" + +#include +#include + +namespace dragon +{ + namespace hw + { + VirtualCPU::VirtualCPU(IMemoryDevice& memory) : m_memory(memory) + { + writeRegister(data::Registers::SP, (uint16_t)(0xFFFF - 1 - 1)); + writeRegister(data::Registers::FP, (uint16_t)(0xFFFF - 1 - 1)); + } + + int16_t VirtualCPU::readRegister(uint8_t reg) + { + if (reg >= data::Registers::Last) return 0x0000; //TODO: Error + return m_registers[reg]; + } + + int16_t VirtualCPU::writeRegister(uint8_t reg, int16_t value) + { + if (reg >= data::Registers::Last) return 0x0000; //TODO: Error + m_registers[reg] = value; + return value; + } + + int8_t VirtualCPU::fetch8(void) + { + uint16_t nextInstAddr = readRegister(data::Registers::IP); + int8_t inst = m_memory.read8(nextInstAddr); + writeRegister(data::Registers::IP, nextInstAddr + 1); + return inst; + } + + int16_t VirtualCPU::fetch16(void) + { + uint16_t nextInstAddr = readRegister(data::Registers::IP); + int16_t inst = m_memory.read16(nextInstAddr); + writeRegister(data::Registers::IP, nextInstAddr + 2); + return inst; + } + + void VirtualCPU::pushToStack(int16_t value) + { + uint16_t stackAddr = readRegister(data::Registers::SP); + m_memory.write16(stackAddr, value); + writeRegister(data::Registers::SP, stackAddr - 2); + m_stackFrameSize += 2; + } + + int16_t VirtualCPU::popFromStack(void) + { + uint16_t nextSP = readRegister(data::Registers::SP) + 2; + writeRegister(data::Registers::SP, nextSP); + int16_t value = m_memory.read16(nextSP); + m_stackFrameSize -= 2; + return value; + } + + void VirtualCPU::pushStackFrame(void) + { + uint16_t argStartAddr = readRegister(data::Registers::SP) + 2; + uint16_t argCount = m_memory.read16(argStartAddr); + if (argCount == 0) + argStartAddr = 0; + else + argStartAddr += (argCount * 2); + + pushToStack(readRegister(data::Registers::R1)); + pushToStack(readRegister(data::Registers::R2)); + pushToStack(readRegister(data::Registers::R3)); + pushToStack(readRegister(data::Registers::R4)); + pushToStack(readRegister(data::Registers::R5)); + pushToStack(readRegister(data::Registers::R6)); + pushToStack(readRegister(data::Registers::R7)); + pushToStack(readRegister(data::Registers::R8)); + pushToStack(readRegister(data::Registers::R9)); + pushToStack(readRegister(data::Registers::R10)); + pushToStack(readRegister(data::Registers::PP)); + pushToStack(readRegister(data::Registers::ACC)); + pushToStack(readRegister(data::Registers::IP)); + pushToStack(m_stackFrameSize + 2); + + writeRegister(data::Registers::PP, argStartAddr); + writeRegister(data::Registers::FP, readRegister(data::Registers::SP)); + m_stackFrameSize = 0; + } + + void VirtualCPU::popStackFrame(void) + { + uint16_t framePointerAddr = readRegister(data::Registers::FP); + writeRegister(data::Registers::SP, framePointerAddr); + m_stackFrameSize = popFromStack(); + uint16_t tmpStackFrameSize = m_stackFrameSize; + + writeRegister(data::Registers::IP, popFromStack()); + writeRegister(data::Registers::ACC, popFromStack()); + writeRegister(data::Registers::PP, popFromStack()); + writeRegister(data::Registers::R10, popFromStack()); + writeRegister(data::Registers::R9, popFromStack()); + writeRegister(data::Registers::R8, popFromStack()); + writeRegister(data::Registers::R7, popFromStack()); + writeRegister(data::Registers::R6, popFromStack()); + writeRegister(data::Registers::R5, popFromStack()); + writeRegister(data::Registers::R4, popFromStack()); + writeRegister(data::Registers::R3, popFromStack()); + writeRegister(data::Registers::R2, popFromStack()); + writeRegister(data::Registers::R1, popFromStack()); + + uint16_t nArgs = popFromStack(); + for (int32_t i = 0; i < nArgs; i++) + popFromStack(); + + writeRegister(data::Registers::FP, framePointerAddr + tmpStackFrameSize); + } + + bool VirtualCPU::readFlag(uint8_t flg) + { + if (flg >= 16) return false; + m_tempFlags.value = readRegister(data::Registers::FL); + return ostd::Bits::get(m_tempFlags, flg); + } + + void VirtualCPU::setFlag(uint8_t flg, bool val) + { + if (flg >= 16) return; + m_tempFlags.value = readRegister(data::Registers::FL); + ostd::Bits::val(m_tempFlags, flg, val); + writeRegister(data::Registers::FL, m_tempFlags.value); + } + + void VirtualCPU::handleInterrupt(uint8_t intValue) + { + // std::cout << "Interrupt: " << ostd::Utils::getHexStr(intValue, true, 1) << "\n"; + uint16_t entryPointer = data::MemoryMapAddresses::IntVector_Start + (intValue * 3); + uint8_t interruptStatus = m_memory.read8(entryPointer); + if (interruptStatus != 0xFF) return; + uint16_t handlerAddress = m_memory.read16(entryPointer + 1); + if (!m_isInInterruptHandler) + { + pushToStack(0); + pushStackFrame(); + } + m_isInInterruptHandler = true; + writeRegister(data::Registers::IP, handlerAddress); + } + + bool VirtualCPU::execute(void) + { + if (m_halt) return false; + m_isDebugBreakPoint = false; + uint8_t inst = fetch8(); + m_currentInst = inst; + switch (inst) + { + case data::OpCodes::NoOp: + { + + } + break; + case data::OpCodes::DEBUG_Break: + { + m_isDebugBreakPoint = true; + } + break; + case data::OpCodes::MovImmReg: + { + uint8_t regAddr = fetch8(); + int16_t literal = fetch16(); + writeRegister(regAddr, literal); + } + break; + case data::OpCodes::MovImmMem: + { + uint16_t addr = fetch16(); + int16_t literal = fetch16(); + m_memory.write16(addr, literal); + } + break; + case data::OpCodes::MovRegReg: + { + uint8_t destRegAddr = fetch8(); + uint8_t srcRegAddr = fetch8(); + int16_t value = readRegister(srcRegAddr); + writeRegister(destRegAddr, value); + } + break; + case data::OpCodes::MovRegMem: + { + uint16_t addr = fetch16(); + uint8_t srcRegAddr = fetch8(); + int16_t value = readRegister(srcRegAddr); + m_memory.write16(addr, value); + } + break; + case data::OpCodes::MovMemReg: + { + uint8_t destRegAddr = fetch8(); + uint16_t addr = fetch16(); + int16_t value = m_memory.read16(addr); + writeRegister(destRegAddr, value); + } + break; + case data::OpCodes::MovDerefRegReg: + { + uint8_t destRegAddr = fetch8(); + uint8_t srcRegAddr = fetch8(); + uint16_t addr = readRegister(srcRegAddr); + int16_t value = m_memory.read16(addr); + writeRegister(destRegAddr, value); + } + break; + case data::OpCodes::MovDerefRegMem: + { + uint16_t destAddr = fetch16(); + uint8_t srcRegAddr = fetch8(); + uint16_t addr = readRegister(srcRegAddr); + int16_t value = m_memory.read16(addr); + m_memory.write16(destAddr, value); + } + break; + case data::OpCodes::MovImmRegOffReg: + { + uint8_t destRegAddr = fetch8(); + uint16_t addr = fetch16(); + uint8_t offRegAddr = fetch8(); + int16_t offset = readRegister(offRegAddr); + int16_t value = m_memory.read16(addr + offset); + writeRegister(destRegAddr, value); + } + break; + case data::OpCodes::MovRegDerefReg: + { + uint8_t destRegAddr = fetch8(); + uint8_t srcRegAddr = fetch8(); + int16_t value = readRegister(srcRegAddr); + uint16_t addr = readRegister(destRegAddr); + m_memory.write16(addr, value); + } + break; + case data::OpCodes::MovMemDerefReg: + { + uint8_t destRegAddr = fetch8(); + uint16_t srcAddr = fetch16(); + int16_t value = m_memory.read16(srcAddr); + uint16_t addr = readRegister(destRegAddr); + m_memory.write16(addr, value); + } + break; + case data::OpCodes::MovImmDerefReg: + { + uint8_t destRegAddr = fetch8(); + int16_t value = fetch16(); + uint16_t addr = readRegister(destRegAddr); + m_memory.write16(addr, value); + } + break; + case data::OpCodes::MovDerefRegDerefReg: + { + uint8_t destRegAddr = fetch8(); + uint8_t srcRegAddr = fetch8(); + uint16_t srcAddr = readRegister(srcRegAddr); + uint16_t destAddr = readRegister(destRegAddr); + int16_t value = m_memory.read16(srcAddr); + m_memory.write16(destAddr, value); + } + break; + case data::OpCodes::MovByteImmMem: + { + uint16_t addr = fetch16(); + int8_t literal = fetch8(); + m_memory.write8(addr, literal); + } + break; + case data::OpCodes::MovByteDerefRegMem: + { + uint16_t destAddr = fetch16(); + uint8_t srcRegAddr = fetch8(); + uint16_t addr = readRegister(srcRegAddr); + int8_t value = m_memory.read8(addr); + m_memory.write8(destAddr, value); + } + break; + case data::OpCodes::MovByteRegDerefReg: + { + uint8_t destRegAddr = fetch8(); + uint8_t srcRegAddr = fetch8(); + int16_t value = readRegister(srcRegAddr); + uint16_t addr = readRegister(destRegAddr); + m_memory.write8(addr, (int8_t)value); + } + break; + case data::OpCodes::MovByteMemDerefReg: + { + uint8_t destRegAddr = fetch8(); + uint16_t srcAddr = fetch16(); + int8_t value = m_memory.read8(srcAddr); + uint16_t addr = readRegister(destRegAddr); + m_memory.write8(addr, value); + } + break; + case data::OpCodes::MovByteImmDerefReg: + { + uint8_t destRegAddr = fetch8(); + int8_t value = fetch8(); + uint16_t addr = readRegister(destRegAddr); + m_memory.write8(addr, value); + } + break; + case data::OpCodes::MovByteDerefRegDerefReg: + { + uint8_t destRegAddr = fetch8(); + uint8_t srcRegAddr = fetch8(); + uint16_t srcAddr = readRegister(srcRegAddr); + uint16_t destAddr = readRegister(destRegAddr); + int8_t value = m_memory.read8(srcAddr); + m_memory.write8(destAddr, value); + } + break; + case data::OpCodes::MovByteMemReg: + { + uint8_t destRegAddr = fetch8(); + uint16_t srcAddr = fetch16(); + int8_t value = m_memory.read8(srcAddr); + writeRegister(destRegAddr, value); + } + break; + case data::OpCodes::MovByteImmReg: + { + uint8_t destRegAddr = fetch8(); + int8_t value = fetch8(); + writeRegister(destRegAddr, value); + } + break; + case data::OpCodes::MovByteDerefRegReg: + { + uint8_t destRegAddr = fetch8(); + uint8_t srcRegAddr = fetch8(); + uint16_t srcAddr = readRegister(srcRegAddr); + int8_t value = m_memory.read8(srcAddr); + writeRegister(destRegAddr, value); + } + break; + case data::OpCodes::MovByteRegMem: + { + uint16_t addr = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + m_memory.write8(addr, (int8_t)(value & 0x00FF)); + } + break; + case data::OpCodes::AddImmReg: + { + uint8_t regAddr = fetch8(); + int16_t literal = fetch16(); + int16_t regValue = readRegister(regAddr); + writeRegister(data::Registers::ACC, regValue + literal); + } + break; + case data::OpCodes::AddRegReg: + { + uint8_t regAddr1 = fetch8(); + uint8_t regAddr2 = fetch8(); + int16_t regValue1 = readRegister(regAddr1); + int16_t regValue2 = readRegister(regAddr2); + writeRegister(data::Registers::ACC, regValue1 + regValue2); + } + break; + case data::OpCodes::SubImmReg: + { + uint8_t regAddr = fetch8(); + int16_t literal = fetch16(); + int16_t regValue = readRegister(regAddr); + writeRegister(data::Registers::ACC, regValue - literal); + } + break; + case data::OpCodes::SubRegReg: + { + uint8_t regAddr1 = fetch8(); + uint8_t regAddr2 = fetch8(); + int16_t regValue1 = readRegister(regAddr1); + int16_t regValue2 = readRegister(regAddr2); + writeRegister(data::Registers::ACC, regValue1 - regValue2); + } + break; + case data::OpCodes::MulImmReg: + { + uint8_t regAddr = fetch8(); + int16_t literal = fetch16(); + int16_t regValue = readRegister(regAddr); + writeRegister(data::Registers::ACC, regValue * literal); + } + break; + case data::OpCodes::MulRegReg: + { + uint8_t regAddr1 = fetch8(); + uint8_t regAddr2 = fetch8(); + int16_t regValue1 = readRegister(regAddr1); + int16_t regValue2 = readRegister(regAddr2); + writeRegister(data::Registers::ACC, regValue1 * regValue2); + } + break; + case data::OpCodes::IncReg: + { + uint8_t regAddr = fetch8(); + int16_t regValue = readRegister(regAddr); + writeRegister(regAddr, regValue + 1); + } + break; + case data::OpCodes::DecReg: + { + uint8_t regAddr = fetch8(); + int16_t regValue = readRegister(regAddr); + writeRegister(regAddr, regValue - 1); + } + break; + case data::OpCodes::RShiftRegImm: + { + int16_t literal = fetch16(); + uint8_t regAddr = fetch8(); + int16_t regValue = readRegister(regAddr); + regValue = regValue >> literal; + writeRegister(regAddr, regValue); + } + break; + case data::OpCodes::RShiftRegReg: + { + uint8_t shiftRegAddr = fetch8(); + uint8_t regAddr = fetch8(); + int16_t shiftValue = readRegister(shiftRegAddr); + int16_t regValue = readRegister(regAddr); + regValue = regValue >> shiftValue; + writeRegister(regAddr, regValue); + } + break; + case data::OpCodes::LShiftRegImm: + { + int16_t literal = fetch16(); + uint8_t regAddr = fetch8(); + int16_t regValue = readRegister(regAddr); + regValue = regValue << literal; + writeRegister(regAddr, regValue); + } + break; + case data::OpCodes::LShiftRegReg: + { + uint8_t shiftRegAddr = fetch8(); + uint8_t regAddr = fetch8(); + int16_t shiftValue = readRegister(shiftRegAddr); + int16_t regValue = readRegister(regAddr); + regValue = regValue << shiftValue; + writeRegister(regAddr, regValue); + } + break; + case data::OpCodes::AndRegImm: + { + int16_t literal = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + writeRegister(data::Registers::ACC, value & literal); + } + break; + case data::OpCodes::AndRegReg: + { + uint8_t andRegAddr = fetch8(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t andValue = readRegister(andRegAddr); + writeRegister(data::Registers::ACC, value & andValue); + } + break; + case data::OpCodes::OrRegImm: + { + int16_t literal = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + writeRegister(data::Registers::ACC, value | literal); + } + break; + case data::OpCodes::OrRegReg: + { + uint8_t andRegAddr = fetch8(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t andValue = readRegister(andRegAddr); + writeRegister(data::Registers::ACC, value | andValue); + } + break; + case data::OpCodes::XorRegImm: + { + int16_t literal = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + writeRegister(data::Registers::ACC, value ^ literal); + } + break; + case data::OpCodes::XorRegReg: + { + uint8_t andRegAddr = fetch8(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t andValue = readRegister(andRegAddr); + writeRegister(data::Registers::ACC, value ^ andValue); + } + break; + case data::OpCodes::NotReg: + { + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + writeRegister(data::Registers::ACC, ~value); + } + break; + case data::OpCodes::JmpNotEqImm: + { + uint16_t addr = fetch16(); + int16_t value = fetch16(); + int16_t accValue = readRegister(data::Registers::ACC); + if (value != accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpNotEqReg: + { + uint16_t addr = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t accValue = readRegister(data::Registers::ACC); + if (value != accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpEqImm: + { + uint16_t addr = fetch16(); + int16_t value = fetch16(); + int16_t accValue = readRegister(data::Registers::ACC); + if (value == accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpEqReg: + { + uint16_t addr = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t accValue = readRegister(data::Registers::ACC); + if (value == accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpGrImm: + { + uint16_t addr = fetch16(); + int16_t value = fetch16(); + int16_t accValue = readRegister(data::Registers::ACC); + if (value > accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpGrReg: + { + uint16_t addr = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t accValue = readRegister(data::Registers::ACC); + if (value > accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpLessImm: + { + uint16_t addr = fetch16(); + int16_t value = fetch16(); + int16_t accValue = readRegister(data::Registers::ACC); + if (value < accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpLessReg: + { + uint16_t addr = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t accValue = readRegister(data::Registers::ACC); + if (value < accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpGeImm: + { + uint16_t addr = fetch16(); + int16_t value = fetch16(); + int16_t accValue = readRegister(data::Registers::ACC); + if (value >= accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpGeReg: + { + uint16_t addr = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t accValue = readRegister(data::Registers::ACC); + if (value >= accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpLeImm: + { + uint16_t addr = fetch16(); + int16_t value = fetch16(); + int16_t accValue = readRegister(data::Registers::ACC); + if (value <= accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::JmpLeReg: + { + uint16_t addr = fetch16(); + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + int16_t accValue = readRegister(data::Registers::ACC); + if (value <= accValue) + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::Jmp: + { + uint16_t addr = fetch16(); + writeRegister(data::Registers::IP, addr); + } + break; + case data::OpCodes::Halt: + { + m_halt = true; + return false; + } + break; + case data::OpCodes::PushImm: + { + int16_t value = fetch16(); + pushToStack(value); + } + break; + case data::OpCodes::PushReg: + { + uint8_t regAddr = fetch8(); + int16_t value = readRegister(regAddr); + pushToStack(value); + } + break; + case data::OpCodes::PopReg: + { + uint8_t regAddr = fetch8(); + int16_t value = popFromStack(); + writeRegister(regAddr, value); + } + break; + case data::OpCodes::CallImm: + { + uint16_t subroutineAddr = fetch16(); + pushStackFrame(); + writeRegister(data::Registers::IP, subroutineAddr); + } + break; + case data::OpCodes::CallReg: + { + uint8_t regAddr = fetch8(); + uint16_t subroutineAddr = readRegister(regAddr); + pushStackFrame(); + writeRegister(data::Registers::IP, subroutineAddr); + } + break; + case data::OpCodes::Ret: + { + popStackFrame(); + } + break; + case data::OpCodes::RetInt: + { + m_isInInterruptHandler = false; + popStackFrame(); + } + break; + case data::OpCodes::Int: + { + uint8_t intValue = fetch8(); + if (!readFlag(data::Flags::InterruptsEnabled)) + return true; + handleInterrupt(intValue); + } + break; + default: + { + data::ErrorHandler::pushError(data::ErrorCodes::CPU_UnknownInstruction, ostd::StringEditor("Unknown instruction: ").add(ostd::Utils::getHexStr(inst, true, 1)).str()); + m_halt = true; + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/hardware/VirtualCPU.hpp b/src/hardware/VirtualCPU.hpp new file mode 100644 index 0000000..4ffdf89 --- /dev/null +++ b/src/hardware/VirtualCPU.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include "IMemoryDevice.hpp" + +namespace dragon +{ + class DragonRuntime; + namespace hw + { + class VirtualCPU + { + public: + VirtualCPU(IMemoryDevice& memory); + int16_t readRegister(uint8_t reg); + int16_t writeRegister(uint8_t reg, int16_t value); + + int8_t fetch8(void); + int16_t fetch16(void); + + void pushToStack(int16_t value); + int16_t popFromStack(void); + + void pushStackFrame(void); + void popStackFrame(void); + + bool readFlag(uint8_t flg); + void setFlag(uint8_t flg, bool val = true); + + void handleInterrupt(uint8_t intValue); + + bool execute(void); + + inline bool isHalted(void) const { return m_halt; } + inline uint8_t getCurrentInstruction(void) const { return m_currentInst; } + inline bool isInDebugBreakPoint(void) const { return m_isDebugBreakPoint; } + + private: + int16_t m_registers[20]; + ostd::BitField_16 m_tempFlags; + IMemoryDevice& m_memory; + uint16_t m_stackFrameSize { 0 }; + bool m_halt { false }; + uint8_t m_currentInst { 0x00 }; + bool m_biosMode { true }; + bool m_isInInterruptHandler { false }; + bool m_isDebugBreakPoint { false }; + + friend class dragon::DragonRuntime; + }; + } +} \ No newline at end of file diff --git a/src/hardware/VirtualHardDrive.cpp b/src/hardware/VirtualHardDrive.cpp new file mode 100644 index 0000000..704abd9 --- /dev/null +++ b/src/hardware/VirtualHardDrive.cpp @@ -0,0 +1,106 @@ +#include "VirtualHardDrive.hpp" + +#include "../tools/GlobalData.hpp" +#include + +namespace dragon +{ + namespace hw + { + void VirtualHardDrive::init(const ostd::String& dataFilePath) + { + m_dataFile.open(dataFilePath, std::ios::out | std::ios::in | std::ios::binary); + if(!m_dataFile) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_UnableToMount, ostd::StringEditor("Unable to mount virtual HardDrive: ").add(dataFilePath).str()); + return; + } + m_fileSize = m_dataFile.tellg(); + m_dataFile.seekg( 0, std::ios::end ); + m_fileSize = (int64_t)m_dataFile.tellg() - m_fileSize; + m_dataFile.seekg( 0, std::ios::beg ); + m_diskID = s_nextDiskID++; + m_initialized = true; + } + + bool VirtualHardDrive::read(uint32_t addr, uint16_t size, ostd::ByteStream& outData) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_Uninitialized, "Attempt to read uninitialized drive."); + return false; + } + if (addr + size > m_fileSize) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ReadOverflow, "Read Overflow on HardDrive."); + return false; + } + uint8_t cell = 0; + outData.clear(); + m_dataFile.seekg(addr); + for (int32_t i = 0; i < size; i++) + { + m_dataFile.read((char*)&cell, sizeof(cell)); + outData.push_back(cell); + } + return true; + } + + bool VirtualHardDrive::write(uint32_t addr, int8_t value) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_Uninitialized, "Attempt to read uninitialized drive."); + return false; + } + if (addr >= m_fileSize) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_WriteOverflow, "Write Overflow on HardDrive."); + return false; + } + m_dataFile.seekp(addr); + m_dataFile.write((char*)(&value), sizeof(value)); + return true; + } + + void VirtualHardDrive::bufferedWrite(int8_t value) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_Uninitialized, "Attempt to read uninitialized drive."); + return; + } + m_writeBuffer.push_back(value); + } + + bool VirtualHardDrive::writeBuffer(uint32_t addr) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_Uninitialized, "Attempt to read uninitialized drive."); + return false; + } + if (m_writeBuffer.size() == 0) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_EmptyBuffer, "Buffered Write empty buffer on HardDrive."); + return false; + } + if (addr + m_writeBuffer.size() > m_fileSize) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_BuffWriteOverflow, "Buffered Write Overflow on HardDrive."); + return false; + } + m_dataFile.seekp(addr); + m_dataFile.write((char*)(&m_writeBuffer[0]), m_writeBuffer.size()); + m_writeBuffer.clear(); + return true; + } + + void VirtualHardDrive::unmount(void) + { + if (!m_initialized) return; + m_dataFile.close(); + m_initialized = false; + } + } +} \ No newline at end of file diff --git a/src/hardware/VirtualHardDrive.hpp b/src/hardware/VirtualHardDrive.hpp new file mode 100644 index 0000000..c08027c --- /dev/null +++ b/src/hardware/VirtualHardDrive.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace dragon +{ + namespace hw + { + class VirtualHardDrive + { + public: + inline VirtualHardDrive(void) { m_initialized = false; } + inline VirtualHardDrive(const ostd::String& dataFilePath) { init(dataFilePath); } + void init(const ostd::String& dataFilePath); + + bool read(uint32_t addr, uint16_t size, ostd::ByteStream& outData); + bool write(uint32_t addr, int8_t value); + void bufferedWrite(int8_t value); + bool writeBuffer(uint32_t addr); + + void unmount(void); + + inline bool isInitialized(void) const { return m_initialized; } + inline uint64_t getSize(void) const { return m_fileSize; }; + inline bool isSame(VirtualHardDrive& vhdd) { return m_diskID == vhdd.m_diskID; } + + private: + std::fstream m_dataFile; + bool m_initialized { false }; + uint64_t m_fileSize { 0 }; + ostd::ByteStream m_writeBuffer; + uint32_t m_diskID { 0 }; + + inline static uint32_t s_nextDiskID = 0; + }; + } +} \ No newline at end of file diff --git a/src/hardware/VirtualIODevices.cpp b/src/hardware/VirtualIODevices.cpp new file mode 100644 index 0000000..4d2cef4 --- /dev/null +++ b/src/hardware/VirtualIODevices.cpp @@ -0,0 +1,655 @@ +#include "VirtualIODevices.hpp" +#include + +#include "VirtualHardDrive.hpp" +#include "MemoryMapper.hpp" +#include "VirtualCPU.hpp" +#include "VirtualRAM.hpp" + +namespace dragon +{ + namespace hw + { + void VirtualBIOS::init(const ostd::String& biosFilePath) + { + bool loaded = ostd::Utils::loadByteStreamFromFile(biosFilePath, m_bios); + if (!loaded) + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_FailedToLoad, "Failed to load BIOS data."); + if (m_bios.size() != 1024) + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_InvalidSize, ostd::StringEditor("Invalid BIOS size: ").add(ostd::Utils::getHexStr(m_bios.size(), true, 2)).str()); + m_initialized = true; + } + + int8_t VirtualBIOS::read8(uint16_t addr) + { + if (addr >= m_bios.size()) + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_InvalidAddress, ostd::StringEditor("Invalid Byte BIOS location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return m_bios[addr]; + } + + int16_t VirtualBIOS::read16(uint16_t addr) + { + if (addr >= m_bios.size() - 1) + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_InvalidAddress, ostd::StringEditor("Invalid Word BIOS location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return ((m_bios[addr + 0] << 8) & 0xFF00U) + | ( m_bios[addr + 1] & 0x00FFU); + } + + int8_t VirtualBIOS::write8(uint16_t addr, int8_t value) + { + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_WriteAttempt, "Attempting to write to BIOS memory map."); + return 0x00; + } + + int16_t VirtualBIOS::write16(uint16_t addr, int16_t value) + { + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_WriteAttempt, "Attempting to write to BIOS memory map."); + return 0x0000; + } + + ostd::ByteStream* VirtualBIOS::getByteStream(void) + { + return &m_bios; + } + + + + + + InterruptVector::InterruptVector(void) + { + uint32_t dataSize = data::MemoryMapAddresses::IntVector_End - data::MemoryMapAddresses::IntVector_Start; + for (int32_t i = 0; i < dataSize; i++) + m_data.push_back(0x00); + } + + int8_t InterruptVector::read8(uint16_t addr) + { + if (addr >= m_data.size()) + data::ErrorHandler::pushError(data::ErrorCodes::IntVector_InvalidAddress, ostd::StringEditor("Invalid Byte IntVector location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return m_data[addr]; + } + + int16_t InterruptVector::read16(uint16_t addr) + { + if (addr >= m_data.size() - 1) + data::ErrorHandler::pushError(data::ErrorCodes::IntVector_InvalidAddress, ostd::StringEditor("Invalid Word IntVector location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return ((m_data[addr + 0] << 8) & 0xFF00U) + | ( m_data[addr + 1] & 0x00FFU); + } + + int8_t InterruptVector::write8(uint16_t addr, int8_t value) + { + if (addr >= m_data.size()) + data::ErrorHandler::pushError(data::ErrorCodes::IntVector_InvalidAddress, ostd::StringEditor("Invalid Word IntVector location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + m_data[addr] = value; + return value; + } + + int16_t InterruptVector::write16(uint16_t addr, int16_t value) + { + if (addr >= m_data.size() - 1) + data::ErrorHandler::pushError(data::ErrorCodes::IntVector_InvalidAddress, ostd::StringEditor("Invalid Word IntVector location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + m_data[addr + 0] = (value >> 8) & 0xFF; + m_data[addr + 1] = value & 0xFF; + return value; + } + + ostd::ByteStream* InterruptVector::getByteStream(void) + { + return &m_data; + } + + + + + + VirtualKeyboard::VirtualKeyboard(void) + { + } + + int8_t VirtualKeyboard::read8(uint16_t addr) + { + return 0x00; + } + + int16_t VirtualKeyboard::read16(uint16_t addr) + { + return 0x0000; + } + + int8_t VirtualKeyboard::write8(uint16_t addr, int8_t value) + { + return 0x00; + } + + int16_t VirtualKeyboard::write16(uint16_t addr, int16_t value) + { + return 0x0000; + } + + ostd::ByteStream* VirtualKeyboard::getByteStream(void) + { + return nullptr; + } + + + + + + VirtualMouse::VirtualMouse(void) + { + } + + int8_t VirtualMouse::read8(uint16_t addr) + { + return 0x00; + } + + int16_t VirtualMouse::read16(uint16_t addr) + { + return 0x0000; + } + + int8_t VirtualMouse::write8(uint16_t addr, int8_t value) + { + return 0x00; + } + + int16_t VirtualMouse::write16(uint16_t addr, int16_t value) + { + return 0x0000; + } + + ostd::ByteStream* VirtualMouse::getByteStream(void) + { + return nullptr; + } + + + + + + VirtualBootloader::VirtualBootloader(void) + { + for (int32_t i = 0; i < 512; i++) + m_mbr.push_back(0); + } + + int8_t VirtualBootloader::read8(uint16_t addr) + { + if (addr >= m_mbr.size()) + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_InvalidAddress, ostd::StringEditor("Invalid Byte MBR location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return m_mbr[addr]; + } + + int16_t VirtualBootloader::read16(uint16_t addr) + { + if (addr >= m_mbr.size() - 1) + data::ErrorHandler::pushError(data::ErrorCodes::BIOS_InvalidAddress, ostd::StringEditor("Invalid Word MBR location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return ((m_mbr[addr + 0] << 8) & 0xFF00U) + | ( m_mbr[addr + 1] & 0x00FFU); + } + + int8_t VirtualBootloader::write8(uint16_t addr, int8_t value) + { + if (addr >= m_mbr.size()) + data::ErrorHandler::pushError(data::ErrorCodes::IntVector_InvalidAddress, ostd::StringEditor("Invalid Word IntVector location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + m_mbr[addr] = value; + return value; + } + + int16_t VirtualBootloader::write16(uint16_t addr, int16_t value) + { + if (addr >= m_mbr.size() - 1) + data::ErrorHandler::pushError(data::ErrorCodes::IntVector_InvalidAddress, ostd::StringEditor("Invalid Word IntVector location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + m_mbr[addr + 0] = (value >> 8) & 0xFF; + m_mbr[addr + 1] = value & 0xFF; + return value; + } + + ostd::ByteStream* VirtualBootloader::getByteStream(void) + { + return &m_mbr; + } + + + + + + VirtualBIOSVideo::VirtualBIOSVideo(VirtualRAM& memory) : m_memory(memory) + { + intptr_t iMemPtr = reinterpret_cast(memory.getByteStream()->data()); + iMemPtr += data::MemoryMapAddresses::BIOSVideo_Start; + m_dataPtr = reinterpret_cast(iMemPtr); + } + + int8_t VirtualBIOSVideo::read8(uint16_t addr) + { + if (addr >= m_size) + data::ErrorHandler::pushError(data::ErrorCodes::BIOSVideo_InvalidAddress, ostd::StringEditor("Invalid read Byte BiosVideo location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return m_dataPtr[addr]; + } + + int16_t VirtualBIOSVideo::read16(uint16_t addr) + { + if (addr >= m_size - 1) + data::ErrorHandler::pushError(data::ErrorCodes::BIOSVideo_InvalidAddress, ostd::StringEditor("Invalid read Word BiosVideo location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return ((m_dataPtr[addr + 0] << 8) & 0xFF00U) + | ( m_dataPtr[addr + 1] & 0x00FFU); + } + + int8_t VirtualBIOSVideo::write8(uint16_t addr, int8_t value) + { + if (addr >= m_size) + data::ErrorHandler::pushError(data::ErrorCodes::BIOSVideo_InvalidAddress, ostd::StringEditor("Invalid read Byte BiosVideo location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + m_dataPtr[addr] = value; + return value; + } + + int16_t VirtualBIOSVideo::write16(uint16_t addr, int16_t value) + { + if (addr >= m_size - 1) + data::ErrorHandler::pushError(data::ErrorCodes::BIOSVideo_InvalidAddress, ostd::StringEditor("Invalid read Word BiosVideo location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + m_dataPtr[addr + 0] = (value >> 8) & 0xFF; + m_dataPtr[addr + 1] = value & 0xFF; + return value; + } + + ostd::ByteStream* VirtualBIOSVideo::getByteStream(void) + { + m_dataCopy.clear(); + m_dataCopy.insert(m_dataCopy.end(), &m_dataPtr[0], &m_dataPtr[m_size]); + return &m_dataCopy; + } + + + + + + namespace interface + { + Disk::Disk(MemoryMapper& memory, VirtualCPU& cpu) : m_memory(memory), m_cpu(cpu) + { + m_data.w_Byte(tRegisters::Signal, tSignalValues::Ignore); + } + + int8_t Disk::read8(uint16_t addr) + { + int8_t value = 0; + if (!m_data.r_Byte(addr, value)) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ControllerReadFailed, "Failed to read byte from HardDrive Controller"); + return 0; + } + return value; + } + + int16_t Disk::read16(uint16_t addr) + { + int16_t value = 0; + if (!m_data.r_Word(addr, value)) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ControllerReadFailed, "Failed to read word from HardDrive Controller"); + return 0; + } + return value; + } + + int8_t Disk::write8(uint16_t addr, int8_t value) + { + if (addr >= tRegisters::FirstReadOnly) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ControllerWriteFailed, "Attempt to write byte to ReadOnly part of HardDrive Controller"); + return 0; + } + if (!m_data.w_Byte(addr, value)) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ControllerWriteFailed, "Failed to write byte to HardDrive Controller"); + return 0; + } + return value; + } + + int16_t Disk::write16(uint16_t addr, int16_t value) + { + if (addr >= tRegisters::FirstReadOnly) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ControllerWriteFailed, "Attempt to write word to ReadOnly part of HardDrive Controller"); + return 0; + } + if (!m_data.w_Word(addr, value)) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ControllerWriteFailed, "Failed to write word to HardDrive Controller"); + return 0; + } + return value; + } + + ostd::ByteStream* Disk::getByteStream(void) + { + return &m_data.getData(); + } + + void Disk::cycleStep(void) + { + uint8_t signal = tSignalValues::Ignore; + m_data.r_Byte(tRegisters::Signal, (int8_t&)signal); + if (m_busy) + { + if (signal == tSignalValues::Cancel) + { + m_data.w_Byte(tRegisters::Status, tStatusValues::Free); + m_data.w_Byte(tRegisters::Signal, tSignalValues::Ignore); + m_busy = false; + return; + } + if (signal != tSignalValues::Ignore) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_InvalidConfiguration, "Invalid HardDrive configuration: register must be set to while busy."); + m_busy = false; + return; + } + uint8_t status = 0; + m_data.r_Byte(tRegisters::Status, (int8_t&)status); + if (status == tStatusValues::Free) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_InvalidConfiguration, "Invalid HardDrive configuration: register set to while busy."); + m_busy = false; + return; + } + uint8_t currentDisk = 0; + uint16_t currentSector = 0, currentAddress = 0, restDataSize = 0, memoryAddress = 0; + m_data.r_Byte(tRegisters::CurrentDisk, (int8_t&)currentDisk); + m_data.r_Word(tRegisters::CurrentSector, (int16_t&)currentSector); + m_data.r_Word(tRegisters::CurrentAddress, (int16_t&)currentAddress); + m_data.r_Word(tRegisters::RestDataSize, (int16_t&)restDataSize); + m_data.r_Word(tRegisters::SourceData, (int16_t&)memoryAddress); + if (m_connectedDisks.count((data::VDiskID)currentDisk) == 0) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_InvalidDiskSelected, "Invalid HardDrive configuration: selected Disk not found."); + m_busy = false; + return; + } + auto& disk = *m_connectedDisks[currentDisk]; + uint32_t hddAddress = 0; + if (currentAddress == 0xFFFF) + { + if (currentSector == 0xFFFF) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_EndOfDisk, "HardDrive Error: Reached end of selected Disk."); + m_busy = false; + return; + } + currentSector++; + currentAddress = 0x0000; + } + hddAddress = (currentSector << 16) | currentAddress; + if (status == tStatusValues::Reading) + { + ostd::ByteStream _data; + if (!disk.read(hddAddress, 1, _data)) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_ReadFailed, "HardDrive Error: Failed to read data."); + m_busy = false; + return; + } + m_memory.write8(memoryAddress, _data[0]); + } + else if (status == tStatusValues::Writing) + { + int8_t dataRead = m_memory.read8(memoryAddress); + if (!disk.write(hddAddress, dataRead)) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_WriteFailed, "HardDrive Error: Failed to write data."); + m_busy = false; + return; + } + } + memoryAddress++; + if (memoryAddress == 0xFFFF) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_MemoryOverflow, "HardDrive Error: Reached end of Memory."); + m_busy = false; + return; + } + restDataSize--; + if (restDataSize == 0) + { + m_data.w_Byte(tRegisters::Status, tStatusValues::Free); + m_data.w_Byte(tRegisters::Signal, tSignalValues::Ignore); + m_busy = false; + m_cpu.handleInterrupt(data::InterruptCodes::DiskInterfaceFFinished); + return; + } + currentAddress++; + m_data.w_Word(tRegisters::CurrentSector, currentSector); + m_data.w_Word(tRegisters::CurrentAddress, currentAddress); + m_data.w_Word(tRegisters::RestDataSize, restDataSize); + m_data.w_Word(tRegisters::SourceData, memoryAddress); + return; + } + if (signal != tSignalValues::Start) return; + uint8_t mode = 0, disk = 0; + uint16_t sector = 0, address = 0, size = 0, srcAddr = 0; + m_data.r_Byte(tRegisters::ModeSelector, (int8_t&)mode); + m_data.r_Byte(tRegisters::DiskSelector, (int8_t&)disk); + m_data.r_Word(tRegisters::SectorSelector, (int16_t&)sector); + m_data.r_Word(tRegisters::AddressSelector, (int16_t&)address); + m_data.r_Word(tRegisters::DataSize, (int16_t&)size); + m_data.r_Word(tRegisters::DataSourceAddress, (int16_t&)srcAddr); + if (mode == tModeValues::Read) + m_data.w_Byte(tRegisters::Status, tStatusValues::Reading); + else if (mode == tModeValues::Write) + m_data.w_Byte(tRegisters::Status, tStatusValues::Writing); + else + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_InvalidConfiguration, "Invalid HardDrive configuration: must be set to or befor starting operations."); + m_busy = false; + return; + } + m_data.w_Byte(tRegisters::CurrentDisk, disk); + m_data.w_Word(tRegisters::CurrentSector, sector); + m_data.w_Word(tRegisters::CurrentAddress, address); + m_data.w_Word(tRegisters::RestDataSize, size); + m_data.w_Word(tRegisters::SourceData, srcAddr); + + m_data.w_Byte(tRegisters::Signal, tSignalValues::Ignore); + m_busy = true; + } + + data::VDiskID Disk::connectDisk(VirtualHardDrive& hdd) + { + for (auto& disk : m_connectedDisks) + { + if (disk.second->isSame(hdd)) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_DiskAlreadyConnected, "Attempt to connect already connected Disk to Controller"); + return 0; + } + } + m_connectedDisks[Disk::s_nextDiskID] = &hdd; + return Disk::s_nextDiskID++; + } + + bool Disk::disconnectDisk(data::VDiskID diskID) + { + if (m_connectedDisks.count(diskID) == 0) + { + data::ErrorHandler::pushError(data::ErrorCodes::HardDrive_DisconnectInvalid, "Attempt to disconnect invalid Disk from Controller"); + return false; + } + m_connectedDisks.erase(diskID); + return true; + } + + + + + + Graphics::Graphics(void) + { + } + + int8_t Graphics::read8(uint16_t addr) + { + return 0x00; + } + + int16_t Graphics::read16(uint16_t addr) + { + return 0x0000; + } + + int8_t Graphics::write8(uint16_t addr, int8_t value) + { + return 0x00; + } + + int16_t Graphics::write16(uint16_t addr, int16_t value) + { + return 0x0000; + } + + ostd::ByteStream* Graphics::getByteStream(void) + { + return nullptr; + } + + + + + + SerialPort::SerialPort(void) + { + } + + int8_t SerialPort::read8(uint16_t addr) + { + return 0x00; + } + + int16_t SerialPort::read16(uint16_t addr) + { + return 0x0000; + } + + int8_t SerialPort::write8(uint16_t addr, int8_t value) + { + return 0x00; + } + + int16_t SerialPort::write16(uint16_t addr, int16_t value) + { + return 0x0000; + } + + ostd::ByteStream* SerialPort::getByteStream(void) + { + return nullptr; + } + + + + + + void CMOS::init(const ostd::String& cmosFilePath) + { + m_size = data::MemoryMapAddresses::CMOS_End - data::MemoryMapAddresses::CMOS_Start + 1; + m_dataFile.open(cmosFilePath, std::ios::out | std::ios::in | std::ios::binary); + if(!m_dataFile) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_UnableToMount, "Unable to mount virtual CMOS chip."); + return; + } + m_fileSize = m_dataFile.tellg(); + m_dataFile.seekg( 0, std::ios::end ); + m_fileSize = (int64_t)m_dataFile.tellg() - m_fileSize; + m_dataFile.seekg( 0, std::ios::beg ); + if (m_fileSize != m_size) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_InvalidSize, ostd::StringEditor("Invalid virtual CMOS chhip size: ").addi(m_fileSize).str()); + return; + } + m_initialized = true; + } + + int8_t CMOS::read8(uint16_t addr) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_Uninitialized, "Attempt to read uninitialized CMOS chip."); + return false; + } + if (addr >= m_size) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_InvalidAddress, ostd::StringEditor("Invalid Byte CMOS location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return false; + } + int8_t value = 0; + m_dataFile.seekg(addr); + m_dataFile.read((char*)&value, sizeof(value)); + return value; + } + + int16_t CMOS::read16(uint16_t addr) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_Uninitialized, "Attempt to read uninitialized CMOS chip."); + return false; + } + if (addr >= m_size - 1) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_InvalidAddress, ostd::StringEditor("Invalid Word CMOS location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return 0; + } + int8_t b1 = read8(addr); + int8_t b2 = read8(addr + 1); + return ((b1 << 8) & 0xFF00U) + | (b2 & 0x00FFU); + } + + int8_t CMOS::write8(uint16_t addr, int8_t value) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_Uninitialized, "Attempt to read uninitialized CMOS chip."); + return false; + } + if (addr >= m_size) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_InvalidAddress, ostd::StringEditor("Invalid Byte CMOS location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return 0; + } + m_dataFile.seekp(addr); + m_dataFile.write((char*)(&value), sizeof(value)); + return value; + } + + int16_t CMOS::write16(uint16_t addr, int16_t value) + { + if (!m_initialized) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_Uninitialized, "Attempt to read uninitialized CMOS chip."); + return false; + } + if (addr >= m_size - 1) + { + data::ErrorHandler::pushError(data::ErrorCodes::CMOS_InvalidAddress, ostd::StringEditor("Invalid Word CMOS location at address: ").add(ostd::Utils::getHexStr(addr, true, 2)).str()); + return 0; + } + int8_t b1 = (value >> 8) & 0xFF; + int8_t b2 = (value & 0xFF); + write8(addr, b1); + write8(addr + 1, b2); + return value; + } + + ostd::ByteStream* CMOS::getByteStream(void) + { + return &m_data; + } + } + } +} \ No newline at end of file diff --git a/src/hardware/VirtualIODevices.hpp b/src/hardware/VirtualIODevices.hpp new file mode 100644 index 0000000..c67ea57 --- /dev/null +++ b/src/hardware/VirtualIODevices.hpp @@ -0,0 +1,237 @@ +#pragma once + +#include "IMemoryDevice.hpp" +#include "../tools/GlobalData.hpp" +#include +#include + +namespace dragon +{ + namespace hw + { + class VirtualHardDrive; + class MemoryMapper; + class VirtualCPU; + class VirtualRAM; + + + class VirtualBIOS : public IMemoryDevice + { + public: + inline VirtualBIOS(void) { m_initialized = 0; } + inline VirtualBIOS(const ostd::String& biosFilePath) { init(biosFilePath); } + void init(const ostd::String& biosFilePath); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + ostd::ByteStream m_bios; + bool m_initialized { false }; + }; + class InterruptVector : public IMemoryDevice + { + public: + InterruptVector(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + ostd::ByteStream m_data; + }; + class VirtualKeyboard : public IMemoryDevice + { + public: + VirtualKeyboard(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + }; + class VirtualMouse : public IMemoryDevice + { + public: + VirtualMouse(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + }; + class VirtualBootloader : public IMemoryDevice + { + public: + VirtualBootloader(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + ostd::ByteStream m_mbr; + }; + class VirtualBIOSVideo : public IMemoryDevice + { + public: struct tRegisters + { + inline static constexpr uint16_t ClearColor = 0x00; + inline static constexpr uint16_t Palette = 0x01; + inline static constexpr uint16_t UseTransparencyOn0x5 = 0x02; + inline static constexpr uint16_t Signal = 0x03; + + inline static constexpr uint16_t VideoMemoryStart = 0x003C; + }; + + public: + VirtualBIOSVideo(VirtualRAM& memory); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + uint16_t m_baseAddress { data::MemoryMapAddresses::BIOSVideo_Start }; + uint16_t m_size { (data::MemoryMapAddresses::BIOSVideo_End + 1) - data::MemoryMapAddresses::BIOSVideo_Start }; + ostd::Byte* m_dataPtr { nullptr }; + ostd::ByteStream m_dataCopy; + VirtualRAM& m_memory; + }; + + namespace interface + { + class Disk : public IMemoryDevice + { + public: struct tRegisters + { + inline static constexpr uint16_t Signal = 0x00; + inline static constexpr uint16_t ModeSelector = 0x01; + inline static constexpr uint16_t DiskSelector = 0x02; + inline static constexpr uint16_t SectorSelector = 0x03; + inline static constexpr uint16_t AddressSelector = 0x05; + inline static constexpr uint16_t DataSize = 0x07; + inline static constexpr uint16_t DataSourceAddress = 0x09; + + inline static constexpr uint16_t FirstReadOnly = 0x0B; + + inline static constexpr uint16_t Status = 0x0B; + inline static constexpr uint16_t CurrentDisk = 0x0C; + inline static constexpr uint16_t CurrentSector = 0x0D; + inline static constexpr uint16_t CurrentAddress = 0x0F; + inline static constexpr uint16_t RestDataSize = 0x11; + inline static constexpr uint16_t SourceData = 0x13; + + }; + + public: struct tSignalValues + { + inline static constexpr uint8_t Start = 0x00; + inline static constexpr uint8_t Cancel = 0x01; + + inline static constexpr uint8_t Ignore = 0xFF; + }; + + public: struct tModeValues + { + inline static constexpr uint8_t Read = 0x00; + inline static constexpr uint8_t Write = 0x01; + }; + + public: struct tStatusValues + { + inline static constexpr uint8_t Free = 0x00; + inline static constexpr uint8_t Writing = 0x01; + inline static constexpr uint8_t Reading = 0x02; + }; + + public: + Disk(MemoryMapper& memory, VirtualCPU& cpu); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + void cycleStep(void); + data::VDiskID connectDisk(VirtualHardDrive& hdd); + bool disconnectDisk(data::VDiskID diskID); + + inline bool isBusy(void) const { return m_busy; } + + private: + ostd::serial::SerialIO m_data { data::MemoryMapAddresses::DiskInterface_End - data::MemoryMapAddresses::DiskInterface_Start }; + bool m_busy { false }; + std::unordered_map m_connectedDisks; + MemoryMapper& m_memory; + VirtualCPU& m_cpu; + + inline static data::VDiskID s_nextDiskID = 0; + + }; + class Graphics : public IMemoryDevice + { + public: + Graphics(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + }; + class SerialPort : public IMemoryDevice + { + public: + SerialPort(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + }; + class CMOS : public IMemoryDevice + { + public: + inline CMOS(void) { m_initialized = false; } + inline CMOS(const ostd::String& cmosFilePath) { init(cmosFilePath); } + void init(const ostd::String& cmosFilePath); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + ostd::ByteStream m_data; + uint16_t m_size { 0 }; + std::fstream m_dataFile; + bool m_initialized { false }; + uint64_t m_fileSize { 0 }; + }; + } + } +} \ No newline at end of file diff --git a/src/hardware/VirtualRAM.cpp b/src/hardware/VirtualRAM.cpp new file mode 100644 index 0000000..4bb212f --- /dev/null +++ b/src/hardware/VirtualRAM.cpp @@ -0,0 +1,45 @@ +#include "VirtualRAM.hpp" + +#include "../tools/GlobalData.hpp" + +namespace dragon +{ + namespace hw + { + VirtualRAM::VirtualRAM(void) + { + m_memory.init(0xFFFF); + } + + int8_t VirtualRAM::read8(uint16_t addr) + { + int8_t outVal = 0; + if (!m_memory.r_Byte(addr, outVal)) return 0x00; //TODO: Error + return outVal; + } + + int16_t VirtualRAM::read16(uint16_t addr) + { + int16_t outVal = 0; + if (!m_memory.r_Word(addr, outVal)) return 0x00; //TODO: Error + return outVal; + } + + int8_t VirtualRAM::write8(uint16_t addr, int8_t value) + { + if (!m_memory.w_Byte(addr, value)) return 0; //TODO: Error + return value; + } + + int16_t VirtualRAM::write16(uint16_t addr, int16_t value) + { + if (!m_memory.w_Word(addr, value)) return 0; //TODO: Error + return value; + } + + ostd::ByteStream* VirtualRAM::getByteStream(void) + { + return &m_memory.getData(); + } + } +} \ No newline at end of file diff --git a/src/hardware/VirtualRAM.hpp b/src/hardware/VirtualRAM.hpp new file mode 100644 index 0000000..472e6b0 --- /dev/null +++ b/src/hardware/VirtualRAM.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "IMemoryDevice.hpp" + +namespace dragon +{ + namespace hw + { + class VirtualRAM : public IMemoryDevice + { + public: + VirtualRAM(void); + int8_t read8(uint16_t addr) override; + int16_t read16(uint16_t addr) override; + int8_t write8(uint16_t addr, int8_t value) override; + int16_t write16(uint16_t addr, int16_t value) override; + + ostd::ByteStream* getByteStream(void) override; + + private: + ostd::serial::SerialIO m_memory; + }; + } +} \ No newline at end of file diff --git a/src/runtime/ConfigLoader.cpp b/src/runtime/ConfigLoader.cpp new file mode 100644 index 0000000..fc5b342 --- /dev/null +++ b/src/runtime/ConfigLoader.cpp @@ -0,0 +1,53 @@ +#include "ConfigLoader.hpp" +#include +#include + +namespace dragon +{ + const tMachineConfig MachineConfigLoader::loadConfig(const ostd::String& configFilePath) + { + tMachineConfig config; + ostd::TextFileBuffer file(configFilePath); + if (!file.exists()) return config; + auto lines = file.getLines(); + for (auto& line : lines) + { + ostd::StringEditor lineEdit = line; + if (!lineEdit.contains("=")) continue; //TODO: Warning + auto tokens = lineEdit.tokenize("="); + if (tokens.count() != 2) continue; //TODO: Warning + lineEdit = tokens.next(); + lineEdit = lineEdit.toLower(); + if (lineEdit.str() == "disks") + { + lineEdit = tokens.next(); + tokens = lineEdit.tokenize(","); + if (tokens.count() == 0) continue; //TODO: Warning + int32_t disk_nr = 0; + while (tokens.hasNext()) + { + lineEdit = tokens.next(); + config.vdisk_paths[disk_nr++] = lineEdit.str(); + } + } + else if (lineEdit.str() == "bios") + { + lineEdit = tokens.next(); + config.bios_path = lineEdit.str(); + } + else if (lineEdit.str() == "cmos") + { + lineEdit = tokens.next(); + config.cmos_path = lineEdit.str(); + } + else continue; //TODO: Warning + } + return validate_machine_config(config); + } + + const tMachineConfig& MachineConfigLoader::validate_machine_config(tMachineConfig& config) //TODO: Implement config validation + { + config.m_valid = true; + return config; + } +} \ No newline at end of file diff --git a/src/runtime/ConfigLoader.hpp b/src/runtime/ConfigLoader.hpp new file mode 100644 index 0000000..89ac58f --- /dev/null +++ b/src/runtime/ConfigLoader.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace dragon +{ + struct tMachineConfig + { + std::map vdisk_paths; + ostd::String bios_path; + ostd::String cmos_path; + + inline bool isValid(void) const { return m_valid; } + + private: + bool m_valid { false }; + + friend class MachineConfigLoader; + }; + + class MachineConfigLoader + { + public: + static const tMachineConfig loadConfig(const ostd::String& configFilePath); + + private: + static const tMachineConfig& validate_machine_config(tMachineConfig& config); + }; +} \ No newline at end of file diff --git a/src/runtime/DragonRuntime.cpp b/src/runtime/DragonRuntime.cpp new file mode 100644 index 0000000..ce04a64 --- /dev/null +++ b/src/runtime/DragonRuntime.cpp @@ -0,0 +1,382 @@ +#include "DragonRuntime.hpp" + +namespace dragon +{ + void DragonRuntime::printRegisters(dragon::hw::VirtualCPU& cpu) + { + out.col("green").p("IP: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::IP), true, 2)); + out.p(" "); + out.col("yellow").p("R1: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R1), true, 2)); + out.p(" "); + out.col("yellow").p("R2: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R2), true, 2)).nl(); + + out.col("green").p("SP: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::SP), true, 2)); + out.p(" "); + out.col("yellow").p("R3: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R3), true, 2)); + out.p(" "); + out.col("yellow").p("R4: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R4), true, 2)).nl(); + + out.col("green").p("FP: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::FP), true, 2)); + out.p(" "); + out.col("yellow").p("R5: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R5), true, 2)); + out.p(" "); + out.col("yellow").p("R6: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R6), true, 2)).nl(); + + out.col("green").p("RV: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::RV), true, 2)); + out.p(" "); + out.col("yellow").p("R7: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R7), true, 2)); + out.p(" "); + out.col("yellow").p("R8: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R8), true, 2)).nl(); + + out.col("green").p("PP: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::PP), true, 2)); + out.p(" "); + out.col("yellow").p("R9: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R9), true, 2)); + out.p(" "); + out.col("yellow").p("R10: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::R10), true, 2)).nl(); + + out.col("green").p("ACC: ").col("white").p(ostd::Utils::getHexStr(cpu.readRegister(dragon::data::Registers::ACC), true, 2)); + out.p(" "); + out.col("yellow").p("FL: ").col("white").p(ostd::Utils::getBinStr(cpu.readRegister(dragon::data::Registers::FL), true, 2)); + } + + void DragonRuntime::processErrors(void) + { + while (dragon::data::ErrorHandler::hasError()) + { + auto err = dragon::data::ErrorHandler::popError(); + out.nl().col(ostd::legacy::ConsoleCol::Red).p("Error ").p(ostd::Utils::getHexStr(err.code, true, 8)).p(": ").p(err.text).nl(); + } + } + + std::vector DragonRuntime::getErrorList(void) + { + std::vector list; + if (!hasError()) + return list; + while (dragon::data::ErrorHandler::hasError()) + { + auto err = dragon::data::ErrorHandler::popError(); + list.push_back(err); + } + return list; + } + + int32_t DragonRuntime::initMachine(const ostd::String& configFilePath, bool verbose, bool trackMachineInfoDiff, bool hideVirtualDisplay) + { + if (verbose) + out.p("Loading machine config: ").p(configFilePath).nl(); + machine_config = dragon::MachineConfigLoader::loadConfig(configFilePath); + if (!machine_config.isValid()) return 1; //TODO: Error + + if (verbose) + out.p(" Initializing virtual display:").nl(); + vDisplay.initialize(800, 600, "font.bmp"); + vDisplay.addWidget(vConsWidg); + vDisplay.setSize(vConsWidg.getw(), vConsWidg.geth()); + if (hideVirtualDisplay) + vDisplay.hide(); + if (verbose) + { + out.p(" Done. (").pi((int)vConsWidg.getw()).p("x").pi((int)vConsWidg.geth()).p(")"); + if (hideVirtualDisplay) + out.p(" - HIDDEN").nl(); + else + out.nl(); + } + + if (machine_config.vdisk_paths.size() == 0) return 2; //TODO: Error + if (verbose) + out.p(" Initializing virtual disks:").nl(); + for (auto const& disk_path : machine_config.vdisk_paths) + { + vDisks[disk_path.first] = dragon::hw::VirtualHardDrive(disk_path.second); + vDiskInterface.connectDisk(vDisks[disk_path.first]); + if (verbose) + out.p(" Disk").pi(disk_path.first).p(" connected: ").p(disk_path.second).nl(); + } + + if (verbose) + out.p(" Loading vBIOS file: ").p(machine_config.bios_path).nl(); + vBIOS.init(machine_config.bios_path); + + if (verbose) + out.p(" Loading vCMOS file: ").p(machine_config.cmos_path).nl(); + vCMOS.init(machine_config.cmos_path); + + if (verbose) + { + out.p(" Initializing Memory Mapper:").nl(); + out.p(" vBIOS: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::BIOS_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::BIOS_End, true, 2)); + out.p(" (remap=false)").nl(); + } + memMap.mapDevice(vBIOS, dragon::data::MemoryMapAddresses::BIOS_Start, dragon::data::MemoryMapAddresses::BIOS_End, false, "vBIOS"); + if (verbose) + { + out.p(" vCMOS: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::CMOS_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::CMOS_End, true, 2)); + out.p(" (remap=true)").nl(); + } + memMap.mapDevice(vCMOS, dragon::data::MemoryMapAddresses::CMOS_Start, dragon::data::MemoryMapAddresses::CMOS_End, true, "vCMOS"); + if (verbose) + { + out.p(" intVec: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::IntVector_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::IntVector_End, true, 2)); + out.p(" (remap=true)").nl(); + } + memMap.mapDevice(intVec, dragon::data::MemoryMapAddresses::IntVector_Start, dragon::data::MemoryMapAddresses::IntVector_End, true, "intVec"); + if (verbose) + { + out.p(" vKeyboard: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::Keyboard_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::Keyboard_End, true, 2)); + out.p(" (remap=false)").nl(); + } + memMap.mapDevice(vKeyboard, dragon::data::MemoryMapAddresses::Keyboard_Start, dragon::data::MemoryMapAddresses::Keyboard_End, false, "vKeyboard"); + if (verbose) + { + out.p(" vMouse: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::Mouse_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::Mouse_End, true, 2)); + out.p(" (remap=false)").nl(); + } + memMap.mapDevice(vMouse, dragon::data::MemoryMapAddresses::Mouse_Start, dragon::data::MemoryMapAddresses::Mouse_End, false, "vMouse"); + if (verbose) + { + out.p(" vBIOSVideo: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::BIOSVideo_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::BIOSVideo_End, true, 2)); + out.p(" (remap=true)").nl(); + } + memMap.mapDevice(vBIOSVideo, dragon::data::MemoryMapAddresses::BIOSVideo_Start, dragon::data::MemoryMapAddresses::BIOSVideo_End, true, "vBIOSVideo"); + if (verbose) + { + out.p(" vMBR: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::MBR_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::MBR_End, true, 2)); + out.p(" (remap=true)").nl(); + } + memMap.mapDevice(vMBR, dragon::data::MemoryMapAddresses::MBR_Start, dragon::data::MemoryMapAddresses::MBR_End, true, "vMBR"); + if (verbose) + { + out.p(" vDiskInterface: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::DiskInterface_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::DiskInterface_End, true, 2)); + out.p(" (remap=true)").nl(); + } + memMap.mapDevice(vDiskInterface, dragon::data::MemoryMapAddresses::DiskInterface_Start, dragon::data::MemoryMapAddresses::DiskInterface_End, true, "vDiskInterface"); + if (verbose) + { + out.p(" vGraphicsInterface: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::VideoCardInterface_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::VideoCardInterface_End, true, 2)); + out.p(" (remap=true)").nl(); + } + memMap.mapDevice(vGraphicsInterface, dragon::data::MemoryMapAddresses::VideoCardInterface_Start, dragon::data::MemoryMapAddresses::VideoCardInterface_End, true, "vGraphicsInterface"); + if (verbose) + { + out.p(" vSerialInterface: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::SerialInterface_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::SerialInterface_End, true, 2)); + out.p(" (remap=true)").nl(); + } + memMap.mapDevice(vSerialInterface, dragon::data::MemoryMapAddresses::SerialInterface_Start, dragon::data::MemoryMapAddresses::SerialInterface_End, true, "vSerialInterface"); + if (verbose) + { + out.p(" RAM: "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::Memory_Start, true, 2)); + out.p(" to "); + out.p(ostd::Utils::getHexStr(dragon::data::MemoryMapAddresses::Memory_End, true, 2)); + out.p(" (remap=false)").nl(); + } + memMap.mapDevice(ram, dragon::data::MemoryMapAddresses::Memory_Start, dragon::data::MemoryMapAddresses::Memory_End, false, "RAM"); + + //Default VideoBios Colors + uint8_t default_bios_video_color = 0x4; + if (verbose) + { + out.p(" Initializing vCPU reset sequence:").nl(); + out.p(" BIOSVideo default colors: ").p(ostd::Utils::getHexStr(default_bios_video_color)).nl(); + } + memMap.write8(dragon::data::MemoryMapAddresses::BIOSVideo_Start, default_bios_video_color); + + uint16_t reset_ip_addr = 0x0000; + if (verbose) + out.p(" Reset IP register: ").p(ostd::Utils::getHexStr(reset_ip_addr, true, 2)).nl(); + cpu.writeRegister(dragon::data::Registers::IP, reset_ip_addr); + + out.nl().nl(); + s_trackMachineInfo = trackMachineInfoDiff; + return 0; + } + + void DragonRuntime::runMachine(int32_t cycleLimit, bool basic_debug, bool step_exec) + { + int32_t cycleCounter = 0; + bool running = true; + uint8_t currentInst = 0x00; + uint32_t instSize = 0; + ostd::ByteStream& ramData = *ram.getByteStream(); + while ((running || vDiskInterface.isBusy()) && cycleCounter++ < 2048) + { + out.clear(); + uint16_t addr = cpu.readRegister(dragon::data::Registers::IP); + uint16_t spAddr = cpu.readRegister(dragon::data::Registers::SP); + running = cpu.execute() && vDisplay.isRunning(); + vDisplay.update(); + currentInst = cpu.getCurrentInstruction(); + vDiskInterface.cycleStep(); + ostd::ConsoleOutputHandler newOut; //TODO: This workaround is not good, this whole file needs to be updated to not use the legacy ConsoleOutputHandler + if ((cpu.isInDebugBreakPoint() || step_exec) && basic_debug) + { + instSize = dragon::data::OpCodes::getInstructionSIze(currentInst); + ostd::Utils::printByteStream(ramData, dragon::data::MemoryMapAddresses::Memory_Start, 16, 10, newOut, addr, instSize, " MEMORY"); + ostd::Utils::printByteStream(*vBIOSVideo.getByteStream(), 0x0000, 16, 16, newOut, -1, 1, " Bios Video"); + printRegisters(cpu); + out.nl().col("yellow").p("Current Instruction: ").col("white").p(ostd::Utils::getHexStr(currentInst, true, 1)); + out.nl(); + out.col(ostd::legacy::ConsoleCol::Magenta).p("######### Reached Break Point at "); + out.p(ostd::Utils::getHexStr(addr, true, 2)); + out.nl().resetColors(); + std::cin.get(); + continue; + } + else if (dragon::data::ErrorHandler::hasError()) + { + if (!basic_debug) + { + processErrors(); + continue; + } + instSize = dragon::data::OpCodes::getInstructionSIze(currentInst); + ostd::Utils::printByteStream(ramData, dragon::data::MemoryMapAddresses::Memory_Start, 16, 10, newOut, addr, instSize, " MEMORY"); + ostd::Utils::printByteStream(*vBIOSVideo.getByteStream(), 0x0000, 16, 16, newOut, -1, 1, " Bios Video"); + printRegisters(cpu); + out.nl().col("yellow").p("Current Instruction: ").col("white").p(ostd::Utils::getHexStr(currentInst, true, 1)); + out.nl(); + out.col(ostd::legacy::ConsoleCol::Red).p("######### Error occurred at "); + out.p(ostd::Utils::getHexStr(addr, true, 2)); + out.nl().resetColors(); + processErrors(); + std::cin.get(); + continue; + } + } + } + + bool DragonRuntime::runStep(std::vector trackedAddresses) + { + __get_machine_footprint(&s_machineInfo, trackedAddresses, true); + bool running = cpu.execute() && vDisplay.isRunning(); + vDisplay.update(); + vDiskInterface.cycleStep(); + __get_machine_footprint(&s_machineInfo, trackedAddresses, false); + return running || vDiskInterface.isBusy(); + } + + void DragonRuntime::forceLoad(const ostd::String& filePath, uint16_t loadAddress) + { + ostd::ByteStream code; + ostd::Utils::loadByteStreamFromFile(filePath, code); + + int16_t index = 0; + for (auto& b : code) + { + ram.write8(dragon::data::MemoryMapAddresses::Memory_Start + loadAddress + index, b); + index++; + } + } + + + void DragonRuntime::__get_machine_footprint(DragonRuntime::tMachineDebugInfo* machineInfo, std::vector trackedAddresses, bool previous) + { + if (!s_trackMachineInfo || machineInfo == nullptr) return; + auto& minfo = *machineInfo; + + if (previous) + { + minfo.vCPUHalt = cpu.m_halt; + minfo.trackedAddresses.clear(); + minfo.previousInstructionTrackedValues.clear(); + minfo.currentInstructionTrackedValues.clear(); + + for (int32_t i = 0; i < 20; i++) + { + if (i < 5) + { + minfo.previousInstructionFootprint[i] = 0; + minfo.currentInstructionFootprint[i] = 0; + } + minfo.previousInstructionRegisters[i] = 0; + minfo.currentInstructionRegisters[i] = 0; + } + + for (auto& addr : trackedAddresses) + minfo.trackedAddresses.push_back(addr); + } + + uint16_t instAddr = cpu.readRegister(data::Registers::IP); + uint8_t instSize = data::OpCodes::getInstructionSIze(memMap.read8(instAddr)); + ostd::String opCode = data::OpCodes::getOpCodeString(memMap.read8(instAddr)); + uint16_t stackFrameSize = cpu.m_stackFrameSize; + + + bool debugBreak = cpu.m_isDebugBreakPoint; + bool intHandler = cpu.m_isInInterruptHandler; + bool biosMode = cpu.m_biosMode; + + if (previous) + { + minfo.previousInstructionAddress = instAddr; + minfo.previousInstructionFootprintSize = instSize; + minfo.previousInstructionStackFrameSize = stackFrameSize; + minfo.previousInstructionOpCode = opCode; + + for (int8_t i = 0; i < instSize; i++) + minfo.previousInstructionFootprint[i] = memMap.read8(instAddr + i); + + for (int8_t i = 0; i < 20; i++) + minfo.previousInstructionRegisters[i] = cpu.readRegister(i); + + for (auto& addr : minfo.trackedAddresses) + minfo.previousInstructionTrackedValues.push_back(memMap.read8(addr)); + + minfo.previousInstructionDebugBreak = debugBreak; + minfo.previousInstructionInterruptHandler = intHandler; + minfo.previousInstructionBiosMode = biosMode; + } + else + { + minfo.currentInstructionAddress = instAddr; + minfo.currentInstructionFootprintSize = instSize; + minfo.currentInstructionStackFrameSize = stackFrameSize; + minfo.currentInstructionOpCode = opCode; + + for (int8_t i = 0; i < instSize; i++) + minfo.currentInstructionFootprint[i] = memMap.read8(instAddr + i); + + for (int8_t i = 0; i < 20; i++) + minfo.currentInstructionRegisters[i] = cpu.readRegister(i); + + for (auto& addr : minfo.trackedAddresses) + minfo.currentInstructionTrackedValues.push_back(memMap.read8(addr)); + + minfo.currentInstructionDebugBreak = debugBreak; + minfo.currentInstructionInterruptHandler = intHandler; + minfo.currentInstructionBiosMode = biosMode; + } + } +} \ No newline at end of file diff --git a/src/runtime/DragonRuntime.hpp b/src/runtime/DragonRuntime.hpp new file mode 100644 index 0000000..3acf05d --- /dev/null +++ b/src/runtime/DragonRuntime.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include "../gui/Window.hpp" +#include "../gui/widgets/VirtualConsoleWidget.hpp" + +#include "../hardware/VirtualCPU.hpp" +#include "../hardware/MemoryMapper.hpp" +#include "../hardware/VirtualRAM.hpp" +#include "../hardware/VirtualIODevices.hpp" +#include "../hardware/VirtualHardDrive.hpp" + +#include "../tools/GlobalData.hpp" + +#include "ConfigLoader.hpp" + +namespace dragon +{ + class DragonRuntime + { + public: struct tMachineDebugInfo { + inline tMachineDebugInfo(void) { } + + uint16_t previousInstructionAddress { 0x0000 }; + uint16_t currentInstructionAddress { 0x0000 }; + int8_t previousInstructionFootprintSize { 0x00 }; + int8_t currentInstructionFootprintSize { 0x00 }; + uint16_t previousInstructionStackFrameSize { 0x00 }; + uint16_t currentInstructionStackFrameSize { 0x00 }; + + ostd::String previousInstructionOpCode { "" }; + ostd::String currentInstructionOpCode { "" }; + + int8_t previousInstructionFootprint[5] { 0x00, 0x00, 0x00, 0x00, 0x00 }; + int8_t currentInstructionFootprint[5] { 0x00, 0x00, 0x00, 0x00, 0x00 }; + int16_t previousInstructionRegisters[20] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + int16_t currentInstructionRegisters[20] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + std::vector trackedAddresses; + std::vector previousInstructionTrackedValues; + std::vector currentInstructionTrackedValues; + + bool previousInstructionDebugBreak { false }; + bool currentInstructionDebugBreak { false }; + bool previousInstructionInterruptHandler { false }; + bool currentInstructionInterruptHandler { false }; + bool previousInstructionBiosMode { false }; + bool currentInstructionBiosMode { false }; + + bool vCPUHalt { false }; + }; + public: + static void printRegisters(dragon::hw::VirtualCPU& cpu); + static void processErrors(void); + static std::vector getErrorList(void); + static int32_t initMachine(const ostd::String& configFilePath, bool verbose = false, bool trackMachineInfoDiff = false, bool hideVirtualDisplay = false); + static void runMachine(int32_t cycleLimit, bool basic_debug, bool step_exec); + static bool runStep(std::vector trackedAddresses = { }); + static void forceLoad(const ostd::String& filePath, uint16_t loadAddress); + + inline static const tMachineDebugInfo& getMachineInfoDiff(void) { return s_machineInfo; } + inline static bool hasError(void) { return data::ErrorHandler::hasError(); } + + private: + static void __get_machine_footprint(tMachineDebugInfo* machineInfo, std::vector trackedAddresses, bool previous); + + public: + inline static ostd::legacy::ConsoleOutputHandler out; + + inline static dragon::hw::MemoryMapper memMap; + inline static dragon::hw::VirtualCPU cpu { memMap }; + inline static dragon::hw::VirtualRAM ram; + inline static dragon::hw::InterruptVector intVec; + inline static dragon::hw::VirtualBIOS vBIOS; + inline static dragon::hw::interface::CMOS vCMOS; + inline static dragon::hw::VirtualBIOSVideo vBIOSVideo { ram }; + inline static dragon::hw::VirtualBootloader vMBR; + inline static dragon::hw::VirtualKeyboard vKeyboard; + inline static dragon::hw::VirtualMouse vMouse; + inline static dragon::hw::interface::Disk vDiskInterface { memMap, cpu }; + inline static dragon::hw::interface::Graphics vGraphicsInterface; + inline static dragon::hw::interface::SerialPort vSerialInterface; + + inline static std::unordered_map vDisks; + + inline static dragon::Window vDisplay; + inline static dragon::VirtualConsoleWidtget vConsWidg { vBIOSVideo, cpu }; + + inline static dragon::tMachineConfig machine_config; + + private: + inline static tMachineDebugInfo s_machineInfo; + inline static bool s_trackMachineInfo { false }; + }; +} \ No newline at end of file diff --git a/src/runtime/runtime_main.cpp b/src/runtime/runtime_main.cpp new file mode 100644 index 0000000..4243f96 --- /dev/null +++ b/src/runtime/runtime_main.cpp @@ -0,0 +1,74 @@ +#include + +#include "DragonRuntime.hpp" + +ostd::ConsoleOutputHandler out; + +struct tCommandLineArgs +{ + ostd::String machine_config_path = ""; + bool basic_debug = false; + bool step_exec = false; + bool verbose_load = false; + int32_t cycle_limit = 0; + bool force_load = false; + ostd::String force_load_file = ""; + uint16_t force_load_mem_offset = 0x00; +}; + +int main(int argc, char** argv) +{ + tCommandLineArgs args; + if (argc < 2) + { + out.fg(ostd::ConsoleColors::Red).p("Error, too few arguments.").reset().nl(); + return 128; + } + else + { + args.machine_config_path = argv[1]; + for (int32_t i = 2; i < argc; i++) + { + ostd::StringEditor edit(argv[i]); + if (edit.str() == "--debug") + args.basic_debug = true; + else if (edit.str() == "--step") + args.step_exec = true; + else if (edit.str() == "--verbose-load") + args.verbose_load = true; + else if (edit.str() == "--limit-cycles") + { + if (i == argc - 1) + break; //TODO: Warning + i++; + edit = argv[i]; + if (!edit.isNumeric()) + continue; //TODO: Error + args.cycle_limit = (int32_t)edit.toInt(); + } + else if (edit.str() == "--force-load") + { + if ((argc - 1) - i < 2) + break; //TODO: Warning + i++; + args.force_load_file = argv[i]; + i++; + edit = argv[i]; + if (!edit.isNumeric()) + continue; //TODO: Error + args.force_load_mem_offset = (uint16_t)edit.toInt(); + args.force_load = true; + } + } + } + + int32_t init_state = dragon::DragonRuntime::initMachine(args.machine_config_path, args.verbose_load); + if (init_state != 0) return init_state; //TODO: Error + + if (args.force_load) + dragon::DragonRuntime::forceLoad(args.force_load_file, args.force_load_mem_offset); + + dragon::DragonRuntime::runMachine(args.cycle_limit, args.basic_debug, args.step_exec); + + return 0; +} diff --git a/src/tools/GlobalData.hpp b/src/tools/GlobalData.hpp new file mode 100644 index 0000000..bb94e4b --- /dev/null +++ b/src/tools/GlobalData.hpp @@ -0,0 +1,424 @@ +#pragma once + +#include +#include + +namespace dragon +{ + namespace data + { + typedef uint32_t VDiskID; + + class ErrorCodes + { + public: + inline static constexpr uint64_t NoError = 0x0000000000000000; + + inline static constexpr uint64_t MM_RegionNotFound = 0x1000000000000000; + + inline static constexpr uint64_t CPU_UnknownInstruction = 0x2000000000000000; + + inline static constexpr uint64_t BIOS_FailedToLoad = 0x3000000000000000; + inline static constexpr uint64_t BIOS_InvalidSize = 0x3000000000000001; + inline static constexpr uint64_t BIOS_WriteAttempt = 0x3000000000000002; + inline static constexpr uint64_t BIOS_InvalidAddress = 0x3000000000000003; + + inline static constexpr uint64_t HardDrive_UnableToMount = 0x4000000000000000; + inline static constexpr uint64_t HardDrive_Uninitialized = 0x4000000000000001; + inline static constexpr uint64_t HardDrive_ReadOverflow = 0x4000000000000002; + inline static constexpr uint64_t HardDrive_WriteOverflow = 0x4000000000000003; + inline static constexpr uint64_t HardDrive_BuffWriteOverflow = 0x4000000000000004; + inline static constexpr uint64_t HardDrive_EmptyBuffer = 0x4000000000000005; + inline static constexpr uint64_t HardDrive_InvalidConfiguration = 0x4000000000000006; + inline static constexpr uint64_t HardDrive_ReadFailed = 0x4000000000000007; + inline static constexpr uint64_t HardDrive_WriteFailed = 0x4000000000000008; + inline static constexpr uint64_t HardDrive_MemoryOverflow = 0x4000000000000009; + inline static constexpr uint64_t HardDrive_InvalidDiskSelected = 0x400000000000000A; + inline static constexpr uint64_t HardDrive_EndOfDisk = 0x400000000000000B; + inline static constexpr uint64_t HardDrive_ControllerReadFailed = 0x400000000000000C; + inline static constexpr uint64_t HardDrive_ControllerWriteFailed = 0x400000000000000D; + inline static constexpr uint64_t HardDrive_DisconnectInvalid = 0x400000000000000E; + inline static constexpr uint64_t HardDrive_DiskAlreadyConnected = 0x400000000000000F; + + inline static constexpr uint64_t CMOS_InvalidAddress = 0x5000000000000000; + inline static constexpr uint64_t CMOS_UnableToMount = 0x5000000000000001; + inline static constexpr uint64_t CMOS_InvalidSize = 0x5000000000000002; + inline static constexpr uint64_t CMOS_Uninitialized = 0x5000000000000003; + + inline static constexpr uint64_t BIOSVideo_InvalidAddress = 0x6000000000000000; + + inline static constexpr uint64_t IntVector_InvalidAddress = 0x7000000000000000; + + }; + + class ErrorHandler + { + public: struct tError + { + uint64_t code; + ostd::String text; + }; + + public: + inline static void pushError(uint64_t code, ostd::String text) { m_errorStack.push_back({ code, text }); } + inline static bool hasError(void) { return m_errorStack.size() > 0; } + inline static tError popError(void) + { + if (m_errorStack.size() == 0) + return { ErrorCodes::NoError, "No Errors." }; + tError err = m_errorStack[m_errorStack.size() - 1]; + m_errorStack.pop_back(); + return err; + } + + private: + inline static std::vector m_errorStack; + }; + + class MemoryMapAddresses + { + public: + inline static constexpr uint16_t BIOS_Start = 0x0000; + inline static constexpr uint16_t BIOS_End = 0x03FF; + + inline static constexpr uint16_t CMOS_Start = 0x0400; + inline static constexpr uint16_t CMOS_End = 0x047F; + + inline static constexpr uint16_t IntVector_Start = 0x0480; + inline static constexpr uint16_t IntVector_End = 0x067F; + + inline static constexpr uint16_t Keyboard_Start = 0x0680; + inline static constexpr uint16_t Keyboard_End = 0x075F; + + inline static constexpr uint16_t Mouse_Start = 0x0760; + inline static constexpr uint16_t Mouse_End = 0x077F; + + inline static constexpr uint16_t BIOSVideo_Start = 0x0780; + inline static constexpr uint16_t BIOSVideo_End = 0x167F; + + inline static constexpr uint16_t MBR_Start = 0x1680; + inline static constexpr uint16_t MBR_End = 0x187F; + + inline static constexpr uint16_t DiskInterface_Start = 0x1880; + inline static constexpr uint16_t DiskInterface_End = 0x18FF; + + inline static constexpr uint16_t VideoCardInterface_Start = 0x1900; + inline static constexpr uint16_t VideoCardInterface_End = 0x19FF; + + inline static constexpr uint16_t SerialInterface_Start = 0x1A00; + inline static constexpr uint16_t SerialInterface_End = 0x1A3F; + + inline static constexpr uint16_t Memory_Start = 0x1A40; + inline static constexpr uint16_t Memory_End = 0xFFFF; + }; + + class IBiosVideoPalette + { + public: + virtual ostd::Color getColor(uint8_t col) = 0; + }; + + class BiosVideoDefaultPalette : public IBiosVideoPalette + { + public: + inline BiosVideoDefaultPalette(void) + { + m_colors.push_back({ 0, 0, 0 }); + m_colors.push_back({ 157, 157, 157 }); + m_colors.push_back({ 255, 255, 255 }); + m_colors.push_back({ 190, 38, 51 }); + m_colors.push_back({ 224, 111, 139 }); + m_colors.push_back({ 73, 60, 43 }); + m_colors.push_back({ 164, 100, 34 }); + m_colors.push_back({ 235, 137, 49 }); + m_colors.push_back({ 247, 226, 107 }); + m_colors.push_back({ 47, 80, 42 }); + m_colors.push_back({ 68, 137, 26 }); + m_colors.push_back({ 163, 206, 39 }); + m_colors.push_back({ 27, 38, 50 }); + m_colors.push_back({ 0, 87, 132 }); + m_colors.push_back({ 49, 162, 242 }); + m_colors.push_back({ 178, 220, 239 }); + } + + inline ostd::Color getColor(uint8_t col) override + { + if (col >= m_colors.size()) return { 0, 0, 0 }; + return m_colors[col]; + } + + private: + std::vector m_colors; + }; + + class Registers + { + public: + inline static constexpr uint8_t IP = 0x00; + inline static constexpr uint8_t SP = 0x01; + inline static constexpr uint8_t FP = 0x02; + inline static constexpr uint8_t RV = 0x03; + inline static constexpr uint8_t PP = 0x04; + inline static constexpr uint8_t FL = 0x05; + inline static constexpr uint8_t ACC = 0x06; + + //TODO: Either add general purpose registers in this space or check when read/write registers + + inline static constexpr uint8_t R1 = 0x0A; + inline static constexpr uint8_t R2 = 0x0B; + inline static constexpr uint8_t R3 = 0x0C; + inline static constexpr uint8_t R4 = 0x0D; + inline static constexpr uint8_t R5 = 0x0E; + inline static constexpr uint8_t R6 = 0x0F; + inline static constexpr uint8_t R7 = 0x10; + inline static constexpr uint8_t R8 = 0x11; + inline static constexpr uint8_t R9 = 0x12; + inline static constexpr uint8_t R10 = 0x13; + + inline static constexpr uint8_t Last = 0x14; + }; + + class Flags + { + public: + inline static constexpr uint8_t InterruptsEnabled = 0; + }; + + class InterruptCodes + { + public: + inline static constexpr uint8_t Keyboard = 0x10; + inline static constexpr uint8_t Mouse = 0x11; + inline static constexpr uint8_t BiosInterrupt = 0x20; + + inline static constexpr uint8_t DiskInterfaceFFinished = 0x80; + inline static constexpr uint8_t BiosVideoScreenRefresh = 0x81; + }; + + class OpCodes + { + public: + inline static constexpr uint8_t NoOp = 0x00; + + inline static constexpr uint8_t DEBUG_Break = 0x01; + + inline static constexpr uint8_t MovImmReg = 0x10; + inline static constexpr uint8_t MovRegReg = 0x11; + inline static constexpr uint8_t MovRegMem = 0x12; + inline static constexpr uint8_t MovMemReg = 0x13; + inline static constexpr uint8_t MovImmMem = 0x14; + inline static constexpr uint8_t MovDerefRegReg = 0x15; + inline static constexpr uint8_t MovImmRegOffReg = 0x16; + inline static constexpr uint8_t MovDerefRegMem = 0x17; + inline static constexpr uint8_t MovRegDerefReg = 0x18; + inline static constexpr uint8_t MovMemDerefReg = 0x19; + inline static constexpr uint8_t MovImmDerefReg = 0x1A; + inline static constexpr uint8_t MovDerefRegDerefReg = 0x1B; + + inline static constexpr uint8_t MovByteImmMem = 0x20; + inline static constexpr uint8_t MovByteDerefRegMem = 0x21; + inline static constexpr uint8_t MovByteImmDerefReg = 0x22; + inline static constexpr uint8_t MovByteRegDerefReg = 0x23; + inline static constexpr uint8_t MovByteMemDerefReg = 0x24; + inline static constexpr uint8_t MovByteDerefRegDerefReg = 0x25; + inline static constexpr uint8_t MovByteMemReg = 0x26; + inline static constexpr uint8_t MovByteImmReg = 0x27; + inline static constexpr uint8_t MovByteDerefRegReg = 0x28; + inline static constexpr uint8_t MovByteRegMem = 0x29; + + inline static constexpr uint8_t AddRegReg = 0x30; + inline static constexpr uint8_t AddImmReg = 0x31; + inline static constexpr uint8_t SubRegReg = 0x32; + inline static constexpr uint8_t SubImmReg = 0x33; + inline static constexpr uint8_t MulRegReg = 0x34; + inline static constexpr uint8_t MulImmReg = 0x35; + + inline static constexpr uint8_t IncReg = 0x40; + inline static constexpr uint8_t DecReg = 0x41; + + inline static constexpr uint8_t PushImm = 0x50; + inline static constexpr uint8_t PushReg = 0x51; + inline static constexpr uint8_t PopReg = 0x52; + inline static constexpr uint8_t CallImm = 0x53; + inline static constexpr uint8_t CallReg = 0x54; + inline static constexpr uint8_t Ret = 0x55; + + inline static constexpr uint8_t LShiftRegImm = 0x60; + inline static constexpr uint8_t LShiftRegReg = 0x61; + inline static constexpr uint8_t RShiftRegImm = 0x62; + inline static constexpr uint8_t RShiftRegReg = 0x63; + inline static constexpr uint8_t AndRegImm = 0x64; + inline static constexpr uint8_t AndRegReg = 0x65; + inline static constexpr uint8_t OrRegImm = 0x66; + inline static constexpr uint8_t OrRegReg = 0x67; + inline static constexpr uint8_t XorRegImm = 0x68; + inline static constexpr uint8_t XorRegReg = 0x69; + inline static constexpr uint8_t NotReg = 0x6A; + + inline static constexpr uint8_t JmpNotEqImm = 0x70; + inline static constexpr uint8_t JmpNotEqReg = 0x71; + inline static constexpr uint8_t JmpEqImm = 0x72; + inline static constexpr uint8_t JmpEqReg = 0x73; + inline static constexpr uint8_t JmpGrImm = 0x74; + inline static constexpr uint8_t JmpGrReg = 0x75; + inline static constexpr uint8_t JmpLessImm = 0x76; + inline static constexpr uint8_t JmpLessReg = 0x77; + inline static constexpr uint8_t JmpGeImm = 0x78; + inline static constexpr uint8_t JmpGeReg = 0x79; + inline static constexpr uint8_t JmpLeImm = 0x7A; + inline static constexpr uint8_t JmpLeReg = 0x7B; + inline static constexpr uint8_t Jmp = 0x7C; + + inline static constexpr uint8_t RetInt = 0xFD; + inline static constexpr uint8_t Int = 0xFE; + inline static constexpr uint8_t Halt = 0xFF; + + inline static ostd::String getOpCodeString(uint8_t opCode) + { + switch (opCode) + { + case data::OpCodes::NoOp: return "NoOp"; + case data::OpCodes::DEBUG_Break: return "debug_break"; + case data::OpCodes::MovImmReg: return "MovImmReg"; + case data::OpCodes::MovImmMem: return "MovImmMem"; + case data::OpCodes::MovRegReg: return "MovRegReg"; + case data::OpCodes::MovRegMem: return "MovRegMem"; + case data::OpCodes::MovMemReg: return "MovMemReg"; + case data::OpCodes::MovDerefRegReg: return "MovDerefRegReg"; + case data::OpCodes::MovDerefRegMem: return "MovDerefRegMem"; + case data::OpCodes::MovImmRegOffReg: return "MovImmRegOffReg"; + case data::OpCodes::MovRegDerefReg: return "MovRegDerefReg"; + case data::OpCodes::MovMemDerefReg: return "MovMemDerefReg"; + case data::OpCodes::MovImmDerefReg: return "MovImmDerefReg"; + case data::OpCodes::MovDerefRegDerefReg: return "MovDerefRegDerefReg"; + case data::OpCodes::MovByteImmMem: return "MovByteImmMem"; + case data::OpCodes::MovByteRegMem: return "MovByteRegMem"; + case data::OpCodes::MovByteDerefRegMem: return "MovByteDerefRegMem"; + case data::OpCodes::MovByteImmDerefReg: return "MovByteImmDerefReg"; + case data::OpCodes::MovByteRegDerefReg: return "MovByteRegDerefReg"; + case data::OpCodes::MovByteMemDerefReg: return "MovByteMemDerefReg"; + case data::OpCodes::MovByteDerefRegDerefReg: return "MovByteDerefRegDerefReg"; + case data::OpCodes::MovByteMemReg: return "MovByteMemReg"; + case data::OpCodes::MovByteImmReg: return "MovByteImmReg"; + case data::OpCodes::MovByteDerefRegReg: return "MovByteDerefRegReg"; + case data::OpCodes::AddImmReg: return "AddImmReg"; + case data::OpCodes::AddRegReg: return "AddRegReg"; + case data::OpCodes::SubImmReg: return "SubImmReg"; + case data::OpCodes::SubRegReg: return "SubRegReg"; + case data::OpCodes::MulImmReg: return "MulImmReg"; + case data::OpCodes::MulRegReg: return "MulRegReg"; + case data::OpCodes::IncReg: return "IncReg"; + case data::OpCodes::DecReg: return "DecReg"; + case data::OpCodes::RShiftRegImm: return "RShiftRegImm"; + case data::OpCodes::RShiftRegReg: return "RShiftRegReg"; + case data::OpCodes::LShiftRegImm: return "LShiftRegImm"; + case data::OpCodes::LShiftRegReg: return "LShiftRegReg"; + case data::OpCodes::AndRegImm: return "AndRegImm"; + case data::OpCodes::AndRegReg: return "AndRegReg"; + case data::OpCodes::OrRegImm: return "OrRegImm"; + case data::OpCodes::OrRegReg: return "OrRegReg"; + case data::OpCodes::XorRegImm: return "XorRegImm"; + case data::OpCodes::XorRegReg: return "XorRegReg"; + case data::OpCodes::NotReg: return "NotReg"; + case data::OpCodes::JmpNotEqImm: return "JmpNotEqImm"; + case data::OpCodes::JmpNotEqReg: return "JmpNotEqReg"; + case data::OpCodes::JmpEqImm: return "JmpEqImm"; + case data::OpCodes::JmpEqReg: return "JmpEqReg"; + case data::OpCodes::JmpGrImm: return "JmpGrImm"; + case data::OpCodes::JmpGrReg: return "JmpGrReg"; + case data::OpCodes::JmpLessImm: return "JmpLessImm"; + case data::OpCodes::JmpLessReg: return "JmpLessReg"; + case data::OpCodes::JmpGeImm: return "JmpGeImm"; + case data::OpCodes::JmpGeReg: return "JmpGeReg"; + case data::OpCodes::JmpLeImm: return "JmpLeImm"; + case data::OpCodes::JmpLeReg: return "JmpLeReg"; + case data::OpCodes::Jmp: return "Jmp"; + case data::OpCodes::Halt: return "Halt"; + case data::OpCodes::PushImm: return "PushImm"; + case data::OpCodes::PushReg: return "PushReg"; + case data::OpCodes::PopReg: return "PopReg"; + case data::OpCodes::CallImm: return "CallImm"; + case data::OpCodes::CallReg: return "CallReg"; + case data::OpCodes::Ret: return "Ret"; + case data::OpCodes::RetInt: return "RetInt"; + case data::OpCodes::Int: return "Int"; + default: return "UNKNOWN_INST"; + } + } + + inline static uint8_t getInstructionSIze(uint8_t opCode) + { + switch (opCode) + { + case data::OpCodes::NoOp: return 1; + case data::OpCodes::DEBUG_Break: return 1; + case data::OpCodes::MovImmReg: return 4; + case data::OpCodes::MovImmMem: return 5; + case data::OpCodes::MovRegReg: return 3; + case data::OpCodes::MovRegMem: return 4; + case data::OpCodes::MovMemReg: return 4; + case data::OpCodes::MovDerefRegReg: return 3; + case data::OpCodes::MovDerefRegMem: return 4; + case data::OpCodes::MovImmRegOffReg: return 5; + case data::OpCodes::MovRegDerefReg: return 3; + case data::OpCodes::MovMemDerefReg: return 4; + case data::OpCodes::MovImmDerefReg: return 4; + case data::OpCodes::MovDerefRegDerefReg: return 3; + case data::OpCodes::MovByteImmMem: return 4; + case data::OpCodes::MovByteRegMem: return 4; + case data::OpCodes::MovByteDerefRegMem: return 4; + case data::OpCodes::MovByteImmDerefReg: return 3; + case data::OpCodes::MovByteRegDerefReg: return 3; + case data::OpCodes::MovByteMemDerefReg: return 4; + case data::OpCodes::MovByteDerefRegDerefReg: return 3; + case data::OpCodes::MovByteMemReg: return 4; + case data::OpCodes::MovByteImmReg: return 3; + case data::OpCodes::MovByteDerefRegReg: return 3; + case data::OpCodes::AddImmReg: return 4; + case data::OpCodes::AddRegReg: return 3; + case data::OpCodes::SubImmReg: return 4; + case data::OpCodes::SubRegReg: return 3; + case data::OpCodes::MulImmReg: return 4; + case data::OpCodes::MulRegReg: return 3; + case data::OpCodes::IncReg: return 2; + case data::OpCodes::DecReg: return 2; + case data::OpCodes::RShiftRegImm: return 4; + case data::OpCodes::RShiftRegReg: return 3; + case data::OpCodes::LShiftRegImm: return 4; + case data::OpCodes::LShiftRegReg: return 3; + case data::OpCodes::AndRegImm: return 4; + case data::OpCodes::AndRegReg: return 3; + case data::OpCodes::OrRegImm: return 4; + case data::OpCodes::OrRegReg: return 3; + case data::OpCodes::XorRegImm: return 4; + case data::OpCodes::XorRegReg: return 3; + case data::OpCodes::NotReg: return 2; + case data::OpCodes::JmpNotEqImm: return 5; + case data::OpCodes::JmpNotEqReg: return 4; + case data::OpCodes::JmpEqImm: return 5; + case data::OpCodes::JmpEqReg: return 4; + case data::OpCodes::JmpGrImm: return 5; + case data::OpCodes::JmpGrReg: return 4; + case data::OpCodes::JmpLessImm: return 5; + case data::OpCodes::JmpLessReg: return 4; + case data::OpCodes::JmpGeImm: return 5; + case data::OpCodes::JmpGeReg: return 4; + case data::OpCodes::JmpLeImm: return 5; + case data::OpCodes::JmpLeReg: return 4; + case data::OpCodes::Jmp: return 3; + case data::OpCodes::Halt: return 1; + case data::OpCodes::PushImm: return 3; + case data::OpCodes::PushReg: return 2; + case data::OpCodes::PopReg: return 2; + case data::OpCodes::CallImm: return 3; + case data::OpCodes::CallReg: return 2; + case data::OpCodes::Ret: return 1; + case data::OpCodes::RetInt: return 1; + case data::OpCodes::Int: return 2; + default: return 0; + } + } + + }; + } +} \ No newline at end of file diff --git a/src/tools/SDLInclude.hpp b/src/tools/SDLInclude.hpp new file mode 100644 index 0000000..56c17b3 --- /dev/null +++ b/src/tools/SDLInclude.hpp @@ -0,0 +1,7 @@ +#pragma once + +#ifdef _WIN32 +#undef __linux__ +#endif + +#include \ No newline at end of file diff --git a/src/tools/Utils.cpp b/src/tools/Utils.cpp new file mode 100644 index 0000000..6f0e150 --- /dev/null +++ b/src/tools/Utils.cpp @@ -0,0 +1,7 @@ +#include "Utils.hpp" +#include +#include + +namespace dragon +{ +} \ No newline at end of file diff --git a/src/tools/Utils.hpp b/src/tools/Utils.hpp new file mode 100644 index 0000000..ac76547 --- /dev/null +++ b/src/tools/Utils.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace dragon +{ + class Utils + { + public: + }; +} \ No newline at end of file diff --git a/src/tools/tools_main.cpp b/src/tools/tools_main.cpp new file mode 100644 index 0000000..b9dc196 --- /dev/null +++ b/src/tools/tools_main.cpp @@ -0,0 +1,122 @@ +#include +#include +#include + +#include "../hardware/VirtualHardDrive.hpp" + +constexpr int ErrorTopLevelTooFewArgs = 1; +constexpr int ErrorTopLevelUnknownTool = 4; + +constexpr int ErrorNewVDiskTooFewArgs = 2; +constexpr int ErrorNewVDiskNonIntSize = 3; +constexpr int ErrorNewVDiskUnable = 5; + +constexpr int ErrorLoadProgTooFewArgs = 6; +constexpr int ErrorLoadProgNonIntAddr = 7; +constexpr int ErrorLoadProgUnableToLoadVDisk = 8; +constexpr int ErrorLoadProgUnableToLoadDataFile = 8; + +constexpr int ErrorNoError = 0; + +ostd::legacy::ConsoleOutputHandler out; + +bool createVirtualHardDrive(uint32_t sizeInBytes, const ostd::String& dataFilePath) +{ + std::ofstream rf(dataFilePath, std::ios::out | std::ios::binary); + if(!rf) return false; + ostd::ByteStream stream; + for (int32_t i = 0; i < sizeInBytes; i++) + stream.push_back(0x00); + rf.write((char*)(&stream[0]), stream.size()); + rf.close(); + return true; +} + +int main(int argc, char** argv) +{ + if (argc < 2) + { + out.col("red").p("Error: too few arguments.").nl(); + out.col("red").p(" Usage: ./dtools [...arguments...]").resetColors().nl(); + return ErrorTopLevelTooFewArgs; + } + ostd::StringEditor tool = argv[1]; + tool = tool.trim().toLower(); + + //Nex Virtual Disk + if (tool.str() == "new-vdisk") + { + if (argc < 4) + { + out.col("red").p("Error: too few arguments.").nl(); + out.col("red").p(" Usage: ./dtools new-vdisk ").resetColors().nl(); + return ErrorNewVDiskTooFewArgs; + } + ostd::String dest = argv[2]; + ostd::String str_size = argv[3]; + if (!ostd::Utils::isInt(str_size)) + { + out.col("red").p("Error: parameter must be integer.").resetColors().nl(); + return ErrorNewVDiskNonIntSize; + } + bool result = createVirtualHardDrive(ostd::Utils::strToInt(str_size), dest); + if (!result) + { + out.col("red").p("Error: Unable to create virtual disk.").resetColors().nl(); + return ErrorNewVDiskUnable; + } + out.col("green").p("Success. Virtual disk created:").nl(); + out.p(" Path: ").p(dest).nl(); + out.p(" Size: ").p(str_size).resetColors().nl(); + } + //Load Program + else if (tool.str() == "load-program") + { + if (argc < 5) + { + out.col("red").p("Error: too few arguments.").nl(); + out.col("red").p(" Usage: ./dtools load-program ").resetColors().nl(); + return ErrorLoadProgTooFewArgs; + } + ostd::String vdisk_file = argv[2]; + ostd::String data_file = argv[3]; + ostd::String str_addr = argv[4]; + if (!ostd::Utils::isInt(str_addr)) + { + out.col("red").p("Error: parameter must be integer.").resetColors().nl(); + return ErrorLoadProgNonIntAddr; + } + dragon::hw::VirtualHardDrive vHDD(vdisk_file); + if (!vHDD.isInitialized()) + { + out.col("red").p("Error: Unable to load virtual disk.").resetColors().nl(); + return ErrorLoadProgUnableToLoadVDisk; + } + ostd::ByteStream code; + if (!ostd::Utils::loadByteStreamFromFile(data_file, code)) + { + out.col("red").p("Error: Unable to load data file.").resetColors().nl(); + return ErrorLoadProgUnableToLoadDataFile; + } + int16_t index = 0; + uint32_t addr = (uint32_t)ostd::Utils::strToInt(str_addr); + for (auto& b : code) + { + vHDD.write(addr + index, b); + index++; + } + vHDD.unmount(); + out.col("green").p("Success. Data writte to Virtual Disk:").nl(); + out.p(" Data Path: ").p(data_file).nl(); + out.p(" Disk Path: ").p(vdisk_file).nl(); + out.p(" Data Address: ").p(ostd::Utils::getHexStr(addr, true, 4)).nl(); + out.p(" Size: ").pi(code.size()).resetColors().nl(); + } + //Unknown + else + { + out.col("red").p("Error: Unknown tool.").resetColors().nl(); + return ErrorTopLevelUnknownTool; + } + return ErrorNoError; +} \ No newline at end of file diff --git a/tools/CppResourceMaker b/tools/CppResourceMaker new file mode 100644 index 0000000000000000000000000000000000000000..6d7872468a124e6c094ad47cc388b78e423b2006 GIT binary patch literal 45144 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 NWvF1etmQ3}{|42p&Jq9s literal 0 HcmV?d00001 diff --git a/tools/build.nr b/tools/build.nr new file mode 100644 index 0000000..fa94e88 --- /dev/null +++ b/tools/build.nr @@ -0,0 +1 @@ +1536 diff --git a/tools/inc_bnr b/tools/inc_bnr new file mode 100644 index 0000000000000000000000000000000000000000..4a0e37bfd303aa1d806667a1e1f15b0e6baf8fef GIT binary patch literal 16880 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