diff --git a/.github/workflows/accuracy.yml b/.github/workflows/accuracy.yml new file mode 100644 index 00000000..58b5e2d7 --- /dev/null +++ b/.github/workflows/accuracy.yml @@ -0,0 +1,79 @@ +name: Accuracy + +on: [push, pull_request, workflow_dispatch] + +jobs: + linux: + runs-on: ubuntu-latest + steps: + - name: Fetch Source Code + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build libsdl2-dev libglew-dev libgtk-3-dev libreadline-dev libedit-dev libcapstone-dev + - name: Build Hades w/ Debugger + run: | + meson build --werror -Dwith_debugger=true + cd build + ninja + - name: Download Test Roms + run: | + # Download BIOS + echo "$BIOS_DATA" | base64 -d | gpg --pinentry-mode loopback --passphrase "$BIOS_KEY" -d -o ./bios.bin + + # Create ROMs directory + mkdir roms + cd roms + + # Download AGS + declare ags_url="$(echo "$AGS_URL" | base64 -d | gpg --pinentry-mode loopback --passphrase "$AGS_KEY" -d)" + wget "$ags_url" -O ags.zip + unzip ags.zip + shred -u ags.zip + mv AGB_*.gba ags.gba + + # Download the remaining testing ROMs + wget https://raw.githubusercontent.com/jsmolka/gba-tests/master/arm/arm.gba + wget https://raw.githubusercontent.com/jsmolka/gba-tests/master/thumb/thumb.gba + wget https://raw.githubusercontent.com/Arignir/Hades-Tests/master/roms/dma-start-delay.gba + wget https://raw.githubusercontent.com/Arignir/Hades-Tests/master/roms/openbus-bios.gba + wget https://raw.githubusercontent.com/Arignir/Hades-Tests/master/roms/timer-basic.gba + env: + BIOS_DATA: ${{ secrets.BIOS_DATA }} + BIOS_KEY: ${{ secrets.BIOS_KEY }} + AGS_URL: ${{ secrets.AGS_URL }} + AGS_KEY: ${{ secrets.AGS_KEY }} + - name: Check Accuracy + run: | + # Setup a fake audio environment + export SDL_AUDIODRIVER=disk + ln -s /dev/null sdlaudio.raw + + # Setup a fake X11 environment + export DISPLAY=:99 + sudo Xvfb -ac "$DISPLAY" -screen 0 1280x1024x24 > /dev/null 2>&1 & + + # Run accuracy checks + python3 ./accuracy/check.py --binary ./build/hades --roms ./roms/ -v + - name: Collect Screenshots + uses: actions/upload-artifact@v3 + if: always() + with: + name: tests-screenshots + path: './.tests_screenshots/' + if-no-files-found: error + - name: Cleanup + if: always() + run: | + if [[ -f ./bios.bin ]]; then + shred -u ./bios.bin + echo "BIOS deleted" + fi + + if [[ -f ./roms/ags.gba ]]; then + shred -u ./roms/ags.gba + echo "AGS deleted" + fi diff --git a/.github/workflows/main.yml b/.github/workflows/build.yml similarity index 96% rename from .github/workflows/main.yml rename to .github/workflows/build.yml index dbae5ce3..54ed21d6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/build.yml @@ -9,10 +9,10 @@ jobs: run: shell: msys2 {0} steps: - - name: 'Sync source code' + - name: Fetch Source Code uses: actions/checkout@v4 with: - submodules: 'recursive' + submodules: recursive - name: Install Dependencies uses: msys2/setup-msys2@v2 with: @@ -35,10 +35,10 @@ jobs: mac-os: runs-on: macos-latest steps: - - name: 'Sync source code' + - name: Fetch Source Code uses: actions/checkout@v4 with: - submodules: 'recursive' + submodules: recursive - name: Install Dependencies run: | brew install meson ninja sdl2 glew create-dmg @@ -130,10 +130,10 @@ jobs: linux: runs-on: ubuntu-latest steps: - - name: 'Sync source code' + - name: Fetch Source Code uses: actions/checkout@v4 with: - submodules: 'recursive' + submodules: recursive - name: Install Dependencies run: | sudo apt-get update diff --git a/.gitignore b/.gitignore index 22743602..32238b38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ /screenshots /build /build.* +/build-* /games /demos *.gba *.bin +config.json +.hades-dbg.history +__pycache__ + diff --git a/accuracy/__init__.py b/accuracy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/accuracy/check.py b/accuracy/check.py new file mode 100755 index 00000000..5513b7da --- /dev/null +++ b/accuracy/check.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 + +import os +import shutil +import filecmp +import textwrap +import argparse +import subprocess +import tempfile +from enum import Enum +from pathlib import Path + + +GREEN = '\033[32m' +YELLOW = '\033[33m' +RED = '\033[31m' +BOLD = '\033[1m' +RESET = '\033[0m' + + +class TestResult(Enum): + PASS = 0 + SKIP = 1 + FAIL = 2 + + +class Test(): + def __init__(self, name: str, rom: str, code: str, screenshot: str, skip: bool = False): + self.name = name + + self.rom = rom + self.code = textwrap.dedent(code) + self.screenshot = screenshot + self.skip = skip + + def run(self, hades_path: Path, rom_directory: Path, config_path: Path, tests_screenshots_directory: Path, verbose: bool): + module_path = Path(os.path.realpath(__file__)).parent + + subprocess.run( + [hades_path, rom_directory / self.rom, '--without-gui', '--config', config_path], + input=self.code, + stdout=None if verbose else subprocess.DEVNULL, + stderr=None if verbose else subprocess.DEVNULL, + text=True, + encoding='utf-8', + check=True, + ) + + if not filecmp.cmp(tests_screenshots_directory / self.screenshot, module_path / 'expected' / self.screenshot, shallow=False): + raise RuntimeError("The screenshot taken during the test doesn't match the expected one.") + + +def main(): + from suite import TESTS_SUITE + + exit_code = 0 + + parser = argparse.ArgumentParser( + prog='Hades Accuracy Checker', + description='Tests the accuracy of Hades, a Gameboy Advance Emulator', + ) + + parser.add_argument( + '--binary', + nargs='?', + default='./hades', + help="Path to Hades' binary", + ) + + parser.add_argument( + '--roms', + nargs='?', + default='./roms', + help="Path to the test ROMS folder", + ) + + parser.add_argument( + '--verbose', + '-v', + action='store_true', + help="Show subcommands output", + ) + + args = parser.parse_args() + + hades_binary = Path(os.getcwd()) / args.binary + rom_directory = Path(os.getcwd()) / args.roms + + config = tempfile.NamedTemporaryFile() + config.write(textwrap.dedent(''' + { + "file": { + "bios": "./bios.bin" + }, + "emulation": { + "skip_bios": true, + "speed": 0, + "unbounded": false, + "backup_storage": { + "autodetect": true, + "type": 0 + }, + "rtc": { + "autodetect": true, + "enabled": true + } + } + } + ''').encode('utf-8')) + config.flush() + + tests_screenshots_directory = Path(os.getcwd()) / '.tests_screenshots' + if tests_screenshots_directory.exists(): + shutil.rmtree(tests_screenshots_directory) + os.mkdir(tests_screenshots_directory) + + print(f"┏━{'━' * 30}━┳━━━━━━┓") + print(f"┃ {'Name':30s} ┃ Res. ┃") + print(f"┣━{'━' * 30}━╋━━━━━━┫") + + for test in TESTS_SUITE: + result = TestResult.FAIL + + try: + if test.skip: + result = TestResult.SKIP + continue + + test.run(hades_binary, rom_directory, config.name, tests_screenshots_directory, args.verbose) + result = TestResult.PASS + except Exception as e: + if args.verbose: + print(f"Error: {e}") + result = TestResult.FAIL + finally: + if result == TestResult.PASS: + pretty_result = f'{BOLD}{GREEN}PASS{RESET}' + elif result == TestResult.SKIP: + pretty_result = f'{BOLD}{YELLOW}SKIP{RESET}' + else: + pretty_result = f'{BOLD}{RED}FAIL{RESET}' + exit_code = 1 + + print(f"┃ {test.name:30s} ┃ {pretty_result} ┃") + + print(f"┗━{'━' * 30}━┻━━━━━━┛") + + exit(exit_code) + + +if __name__ == '__main__': + main() diff --git a/accuracy/expected/ags_01.png b/accuracy/expected/ags_01.png new file mode 100644 index 00000000..5ef7357b Binary files /dev/null and b/accuracy/expected/ags_01.png differ diff --git a/accuracy/expected/arm.png b/accuracy/expected/arm.png new file mode 100644 index 00000000..512904da Binary files /dev/null and b/accuracy/expected/arm.png differ diff --git a/accuracy/expected/dma_start_delay.png b/accuracy/expected/dma_start_delay.png new file mode 100644 index 00000000..75c961d0 Binary files /dev/null and b/accuracy/expected/dma_start_delay.png differ diff --git a/accuracy/expected/openbus_bios.png b/accuracy/expected/openbus_bios.png new file mode 100644 index 00000000..62797510 Binary files /dev/null and b/accuracy/expected/openbus_bios.png differ diff --git a/accuracy/expected/thumb.png b/accuracy/expected/thumb.png new file mode 100644 index 00000000..512904da Binary files /dev/null and b/accuracy/expected/thumb.png differ diff --git a/accuracy/expected/timer_basic.png b/accuracy/expected/timer_basic.png new file mode 100644 index 00000000..f19b9e16 Binary files /dev/null and b/accuracy/expected/timer_basic.png differ diff --git a/accuracy/suite.py b/accuracy/suite.py new file mode 100644 index 00000000..6413ae0d --- /dev/null +++ b/accuracy/suite.py @@ -0,0 +1,68 @@ +from typing import List +from check import Test + +TESTS_SUITE: List[Test] = [ + + # Jsmolka's arm.gba & thumg.gba + # https://github.com/jsmolka/gba-tests + Test( + name="Jsmolka - Arm.gba", + rom='arm.gba', + code=''' + frame 10 + screenshot ./.tests_screenshots/arm.png + ''', + screenshot='arm.png', + ), + Test( + name="Jsmolka - Thumb.gba", + rom='thumb.gba', + code=''' + frame 10 + screenshot ./.tests_screenshots/thumb.png + ''', + screenshot='thumb.png', + ), + + # Hades Tests + # https://github.com/Arignir/Hades-Tests + Test( + name="Hades Tests - DMA Start Delay", + rom='dma-start-delay.gba', + code=''' + frame 20 + screenshot ./.tests_screenshots/dma_start_delay.png + ''', + screenshot='dma_start_delay.png', + skip=True, + ), + Test( + name="Hades Tests - Openbus BIOS", + rom='openbus-bios.gba', + code=''' + frame 20 + screenshot ./.tests_screenshots/openbus_bios.png + ''', + screenshot='openbus_bios.png', + ), + Test( + name="Hades Tests - Timer Basic", + rom='timer-basic.gba', + code=''' + frame 20 + screenshot ./.tests_screenshots/timer_basic.png + ''', + screenshot='timer_basic.png', + ), + + # AGS + Test( + name="AGS - Aging Tests", + rom='ags.gba', + code=''' + frame 425 + screenshot ./.tests_screenshots/ags_01.png + ''', + screenshot='ags_01.png', + ) +] diff --git a/source/app/config.c b/source/app/config.c index d166d97f..90dcd88f 100644 --- a/source/app/config.c +++ b/source/app/config.c @@ -72,7 +72,7 @@ app_config_load( if (mjson_get_number(data, data_len, "$.emulation.speed", &d)) { app->emulation.speed = (int)d; - app->emulation.speed = max(1, min(app->emulation.speed, 5)); + app->emulation.speed = max(0, min(app->emulation.speed, 5)); } if (mjson_get_bool(data, data_len, "$.emulation.backup_storage.autodetect", &b)) { diff --git a/source/app/dbg/lang/lexer.c b/source/app/dbg/lang/lexer.c index 567677c5..75c81bfc 100644 --- a/source/app/dbg/lang/lexer.c +++ b/source/app/dbg/lang/lexer.c @@ -66,6 +66,7 @@ debugger_lang_lexe( while (input[i]) { switch (input[i]) { case '.': + case '_': case 'a' ... 'z': case 'A' ... 'Z': { /* Lexe the whole identifier */ @@ -73,7 +74,7 @@ debugger_lang_lexe( size_t j; j = 0; - while (isalnum(input[i + j]) || input[i + j] == '.' || input[i + j] == '/') { + while (isalnum(input[i + j]) || input[i + j] == '.' || input[i + j] == '_' || input[i + j] == '/') { ++j; } diff --git a/source/app/main.c b/source/app/main.c index ac96505d..39651975 100644 --- a/source/app/main.c +++ b/source/app/main.c @@ -104,6 +104,8 @@ main( logln(HS_INFO, "Welcome to Hades v" HADES_VERSION); logln(HS_INFO, "========================="); + return EXIT_SUCCESS; + if (app.args.with_gui) { app_sdl_init(&app); @@ -158,7 +160,7 @@ main( uint64_t sdl_counters[2]; float elapsed_ms; - sdl_counters[0] = SDL_GetPerformanceCounter(); + sdl_counters[0] = 0; //SDL_GetPerformanceCounter(); app_emulator_process_all_notifs(&app);