srctree

Johan parent 85899254 9310faef 46703ba7
Merge pull request #370 from johnor/ppu-test-program

Add rom test program
.github/workflows/ci-nestest.yaml added: 379, removed: 8, total 371
@@ -19,8 +19,16 @@ jobs:
os: ubuntu-20.04
compiler: clang
version: "10"
nestest: true
cmake-args: -DADDRESS_SANITIZER=ON -DUNDEFINED_SANITIZER=ON -DCMAKE_BUILD_TYPE=Debug
 
- name: romtest-clang-10
os: ubuntu-20.04
compiler: clang
version: "10"
romtest: true
cmake-args: -DADDRESS_SANITIZER=ON -DUNDEFINED_SANITIZER=ON -DCMAKE_BUILD_TYPE=Release
 
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
@@ -47,9 +55,10 @@ jobs:
 
- name: Build
run: |
cmake --build build --target nestest
cmake --build build --target nestest romtest
 
- name: Run Nestest
if: matrix.nestest
env:
ASAN_OPTIONS: "symbolize=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1"
UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1"
@@ -58,11 +67,23 @@ jobs:
wget https://robinli.eu/f/nestest.log
./build/nestest/nestest nestest.nes > nestest-output.log
 
- name: Diff output
- name: Diff nestest output
if: matrix.nestest
run: python3 nestest/test_nestest.py --nestest-log nestest.log --nestest-rom nestest.nes --nestest-bin build/nestest/nestest --min-matching-lines 8970
 
- name: Upload artifacts
if: matrix.nestest
uses: actions/upload-artifact@v1
with:
name: nestest-log
path: nestest-output.log
 
- name: Run rom tests
if: matrix.romtest
env:
ASAN_OPTIONS: "symbolize=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1"
UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1"
run: |
git clone https://github.com/christopherpow/nes-test-roms.git
git -C nes-test-roms checkout 95d8f621ae55cee0d09b91519a8989ae0e64753b
python3 romtest/test_rom.py --romtest-bin build/romtest/romtest --verbose
 
CMakeLists.txt added: 379, removed: 8, total 371
@@ -15,3 +15,4 @@ add_subdirectory(core)
add_subdirectory(disassembler)
add_subdirectory(nes)
add_subdirectory(nestest)
add_subdirectory(romtest)
 
filename was Deleted added: 379, removed: 8, total 371
@@ -0,0 +1,10 @@
cc_binary(
name = "romtest",
srcs = ["src/main.cpp"],
deps = [
"//core",
"//disassembler",
"//nes",
"@fmtlib",
],
)
 
filename was Deleted added: 379, removed: 8, total 371
@@ -0,0 +1,26 @@
project(romtest)
 
add_executable(${PROJECT_NAME}
src/main.cpp
)
 
target_link_libraries(${PROJECT_NAME}
PRIVATE
n_e_s::nes
n_e_s::core
n_e_s::disassembler
n_e_s::warnings
fmt
)
 
target_compile_features(${PROJECT_NAME}
PRIVATE
cxx_std_20
)
 
set_target_properties(${PROJECT_NAME}
PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS NO
)
 
filename was Deleted added: 379, removed: 8, total 371
@@ -0,0 +1,56 @@
#include <fstream>
#include <stdexcept>
#include <string>
 
#include <fmt/core.h>
 
#include "nes/core/immu.h"
#include "nes/core/imos6502.h"
#include "nes/core/ippu.h"
#include "nes/core/opcode.h"
#include "nes/disassembler.h"
#include "nes/nes.h"
 
namespace {
 
void print_nametable(const n_e_s::nes::Nes &nes) {
for (uint16_t y = 0; y < 30; ++y) {
for (uint16_t x = 0; x < 32; ++x) {
// $2000-$23FF Nametable 0
// $2400-$27FF Nametable 1
// $2800-$2BFF Nametable 2
// $2C00-$2FFF Nametable 3
const int nametable = 0;
const uint16_t address = 0x2000 + 0x0400 * nametable + y * 32 + x;
const uint8_t tile_index = nes.ppu_mmu().read_byte(address);
fmt::print("{:02X},", tile_index);
}
fmt::print("\n");
}
}
 
} // namespace
 
int main(int argc, char **argv) try {
if (argc != 3) {
fmt::print(stderr, "Expected two argument; <rom.nes> <cycles>\n");
return 1;
}
 
n_e_s::nes::Nes nes;
const std::string rom = argv[1];
const int cycles = std::stoi(argv[2]);
std::ifstream fs(rom, std::ios::binary);
nes.load_rom(fs);
 
fmt::print("Running rom: \"{}\"\n", rom);
fmt::print("Cycles: \"{}\"\n", cycles);
 
for (int i = 0; i < cycles; ++i) {
nes.execute();
}
print_nametable(nes);
} catch (const std::exception &e) {
fmt::print(stderr, "Exception: {}\n", e.what());
return 1;
}
 
filename was Deleted added: 379, removed: 8, total 371
@@ -0,0 +1,257 @@
import argparse
import pathlib
import subprocess
import sys
import typing
from dataclasses import dataclass
from pathlib import Path
 
NES_SCRREN_ROWS = 30
NES_SCRREN_COLS = 32
 
 
@dataclass
class Test:
rom: str
pass_pattern: str
failing: bool = False
cycles: int = 100000000
 
 
TESTS = [
# PPU tests
Test(
rom="vbl_nmi_timing/1.frame_basics.nes", pass_pattern="PASSED", cycles=100000000
),
Test(
rom="vbl_nmi_timing/2.vbl_timing.nes",
pass_pattern="PASSED",
cycles=100000000,
failing=True,
), # FAILED #8
Test(
rom="vbl_nmi_timing/3.even_odd_frames.nes",
pass_pattern="PASSED",
cycles=100000000,
failing=True,
), # FAILED #3
Test(
rom="vbl_nmi_timing/4.vbl_clear_timing.nes",
pass_pattern="PASSED",
cycles=100000000,
),
Test(
rom="vbl_nmi_timing/5.nmi_suppression.nes",
pass_pattern="PASSED",
cycles=100000000,
failing=True,
), # FAILED #3
Test(
rom="vbl_nmi_timing/6.nmi_disable.nes",
pass_pattern="PASSED",
cycles=100000000,
failing=True,
), # FAILED #2
Test(
rom="vbl_nmi_timing/7.nmi_timing.nes",
pass_pattern="PASSED",
cycles=100000000,
failing=True,
), # FAILED #2
# CPU tests
Test(
rom="cpu_dummy_reads/cpu_dummy_reads.nes", pass_pattern="PASSED", failing=True
), # Unsupported mapper 3
Test(
rom="cpu_dummy_writes/cpu_dummy_writes_ppumem.nes",
pass_pattern="PASSED",
failing=True,
cycles=100000000,
), # Failed #9
Test(
rom="cpu_dummy_writes/cpu_dummy_writes_oam.nes",
pass_pattern="PASSED",
failing=True,
cycles=100000000,
), # Failed #6
Test(
rom="cpu_exec_space/test_cpu_exec_space_ppuio.nes",
pass_pattern="PASSED",
cycles=10000000,
failing=True,
), # Failed #3, ppu open bus
Test(
rom="cpu_timing_test6/cpu_timing_test.nes",
pass_pattern="PASSED",
cycles=1000000000,
),
Test(
rom="instr_test-v5/rom_singles/01-basics.nes",
pass_pattern="Passed",
cycles=10000000,
),
Test(
rom="instr_test-v5/rom_singles/02-implied.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/03-immediate.nes",
pass_pattern="Passed",
cycles=100000000,
failing=True,
), # Missing opcode 0x82
Test(
rom="instr_test-v5/rom_singles/04-zero_page.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/05-zp_xy.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/06-absolute.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/07-abs_xy.nes",
pass_pattern="Passed",
cycles=100000000,
failing=True,
), # Missing opcode 0x9c
Test(
rom="instr_test-v5/rom_singles/08-ind_x.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/09-ind_y.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/10-branches.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/11-stack.nes",
pass_pattern="Passed",
cycles=100000000,
),
Test(
rom="instr_test-v5/rom_singles/12-jmp_jsr.nes",
pass_pattern="Passed",
cycles=10000000,
),
Test(
rom="instr_test-v5/rom_singles/13-rts.nes",
pass_pattern="Passed",
cycles=10000000,
),
Test(
rom="instr_test-v5/rom_singles/14-rti.nes",
pass_pattern="Passed",
cycles=10000000,
),
Test(
rom="instr_test-v5/rom_singles/15-brk.nes",
pass_pattern="Passed",
cycles=10000000,
failing=True,
),
Test(
rom="instr_test-v5/rom_singles/16-special.nes",
pass_pattern="Passed",
cycles=10000000,
),
]
 
 
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--rom-repo-path", type=pathlib.Path, default="nes-test-roms")
parser.add_argument("--romtest-bin", type=pathlib.Path)
parser.add_argument("--test", help="If set, only run this test")
parser.add_argument(
"--verbose", "-v", action="store_true", help="Print more debug data if set"
)
args = parser.parse_args()
return args
 
 
def run_test(
romtest_bin: Path, rom_repo_path: Path, test: Test
) -> typing.Dict[typing.Tuple[int, int], int]:
rom = rom_repo_path / test.rom
print(f'Running rom: "{rom}" with binary: "{romtest_bin}"')
output = subprocess.run(
[romtest_bin, rom, str(test.cycles)], capture_output=True, text=True
)
if output.returncode != 0:
print(
f"ERROR! Could not run rom: {rom}\nstdout: {output.stdout}\nstderr: {output.stderr}"
)
return {}
grid: typing.Dict[typing.Tuple[int, int], int] = {}
for row, line in enumerate(output.stdout.splitlines()[2:]):
for col, tile_index_hex in enumerate(line.split(",")):
if tile_index_hex:
grid[(row, col)] = int(tile_index_hex, 16)
return grid
 
 
def is_ascii_in_output(grid: typing.Dict, ascii_text: str, verbose: bool) -> bool:
for row in range(0, NES_SCRREN_ROWS):
line_ascii = "".join(chr(grid[(row, col)]) for col in range(0, NES_SCRREN_COLS))
if verbose and line_ascii.strip():
print(line_ascii)
if ascii_text in line_ascii:
return True
return False
 
 
def main():
args = parse_args()
success = True
 
if args.test:
print(f"Running only tests matching: {args.test}")
 
for test in TESTS:
if args.test and args.test not in test.rom:
continue
grid = run_test(
romtest_bin=args.romtest_bin, rom_repo_path=args.rom_repo_path, test=test
)
if grid:
test_passed = is_ascii_in_output(
grid=grid, ascii_text=test.pass_pattern, verbose=args.verbose
)
else:
test_passed = False
 
if test_passed:
print("Success!")
else:
print("Fail!")
 
if test.failing and not test_passed:
print("WARNING! Test did not pass but is marked as failing")
elif test.failing and test_passed:
print("ERROR! Test passed but is marked as failing")
else:
success &= test_passed
 
if success:
print("All tests passed!")
else:
print("Some tests failed")
sys.exit(0 if success else 1)
 
 
if __name__ == "__main__":
main()