Emulator 8-bit cycle-accurate menuntut kepatuhan timing mikroarsitektur pada setiap transisi clock, bukan sekadar ketepatan interpretasi instruksi per instruksi. Kesalahan satu siklus pada memory access, penanganan branch instruction, atau interrupt acknowledgement dapat merusak sinkronisasi sub-sistem periferal. Artikel ini menguraikan arsitektur test harness deterministik dan strategi pencegahan flaky regression test pada pipeline CI emulator sistem 8-bit.
Penyebab Utama Flaky Test pada Emulator 8-Bit
Flaky test pada emulator retro umumnya bukan disebabkan oleh konkurensi multi-threading di tingkat aplikasi, melainkan non-determinisme buatan pada harness dan ketergantungan timing periferal eksternal:
- Ketergantungan Wall-Clock: Mengatur throttle eksekusi siklus menggunakan
sleep()atau timer OS berbasis millisecond. Timer thread host OS selalu mengalami jitter. - State Awal Uninitialized: Register atau blok RAM internal mengasumsikan nilai default nol secara implisit, padahal hardware aslinya memiliki pola power-on state acak atau bernilai fixed non-zero (misalnya nilai floating bus saat open bus reading).
- Interrupt Timing Jitter: Pemicuan NMI (Non-Maskable Interrupt) atau IRQ dari thread asynchronous luar yang tidak sinkron terhadap sub-cycle CPU clock.
Pola Golden Master: Snapshot Register dan Memory Bus
Strategi paling tangguh untuk mendeteksi regresi mikroarsitektur adalah Golden Master Verification. Emulator dieksekusi melawan serangkaian test ROM referensi publik (seperti suite instruksi CPU komprehensif), kemudian sistem membandingkan log trace baris-per-baris dengan output referensi yang valid.
Format log per siklus harus mencakup data esensial:
// Format: [Cycle] PC: ADDR | OP: HEX | A:XX X:XX Y:XX SP:XX P:FLAGS | BUS: [R/W] ADDR:VAL
00004128 C000 LDA #$10 A:00 X:00 Y:00 SP:FD P:nv--dIzC BUS: R C001:10
00004129 C002 STA $2000 A:10 X:00 Y:00 SP:FD P:nv--dIzC BUS: W 2000:10Perbandingan state ini menangkap dua hal sekaligus: status register internal (A, X, Y, Status Flag, Stack Pointer) dan aktivitas memory bus (apakah read/write cycle terjadi pada clock cycle yang tepat di bus data).
Implementasi Test Harness Deterministik
Harness pengujian harus mengisolasi emulator secara total dari OS time. CPU, Memory Bus, dan periferal digerakkan oleh loop tick diskret tunggal. Berikut contoh implementasi harness deterministik dalam bahasa C murni:
#include <stdint.h>
#include <stdbool.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
typedef struct {
uint16_t pc;
uint8_t a, x, y, sp, p;
uint64_t cycles;
} CpuState;
typedef struct {
uint8_t memory[0x10000];
CpuState cpu;
bool bus_read_cycle;
bool bus_write_cycle;
uint16_t last_addr;
uint8_t last_val;
} SystemBus;
// Step single master clock cycle
void system_clock_tick(SystemBus *sys) {
// Reset per-cycle bus activity indicators
sys->bus_read_cycle = false;
sys->bus_write_cycle = false;
// Eksekusi logic cycle CPU mikro-instruksi
// ponytail: CPU decode logic disederhanakan; sambungkan core CPU penuh di sini
sys->cpu.cycles++;
}
// Harness eksekusi deterministik
bool run_golden_test(SystemBus *sys, const uint8_t *rom_data, size_t rom_size, uint64_t max_cycles, const char *golden_log_path) {
memset(sys, 0, sizeof(SystemBus));
memcpy(&sys->memory[0x8000], rom_data, rom_size);
sys->cpu.pc = 0x8000;
sys->cpu.sp = 0xFD;
sys->cpu.p = 0x24;
FILE *golden = fopen(golden_log_path, "r");
assert(golden && "File golden master log wajib tersedia");
char expected_line[128];
char actual_line[128];
while (sys->cpu.cycles < max_cycles) {
system_clock_tick(sys);
snprintf(actual_line, sizeof(actual_line), "CYC:%08llu PC:%04X A:%02X X:%02X P:%02X",
sys->cpu.cycles, sys->cpu.pc, sys->cpu.a, sys->cpu.x, sys->cpu.p);
if (fgets(expected_line, sizeof(expected_line), golden) == NULL) {
fclose(golden);
return false; // Log actual lebih panjang dari master
}
// Hapus newline
expected_line[strcspn(expected_line, "\r\n")] = 0;
if (strcmp(actual_line, expected_line) != 0) {
fprintf(stderr, "MISMATCH CYCLE %llu!\nExp: %s\nAct: %s\n",
sys->cpu.cycles, expected_line, actual_line);
fclose(golden);
return false;
}
}
fclose(golden);
return true;
}
int main(void) {
SystemBus sys;
const uint8_t dummy_rom[] = { 0xEA, 0xEA, 0xEA }; // NOP; NOP; NOP
// Ponytail: Validasi baseline loop runner tanpa file external IO
sys.cpu.cycles = 0;
system_clock_tick(&sys);
assert(sys.cpu.cycles == 1);
return 0;
}Isolasi Non-Deterministik: Interrupts dan Memory Mapped I/O
Emulator rentan gagal dalam verifikasi otomatis jika pemicuan interupsi diimplementasikan secara acak. Solusi deterministik membutuhkan antrean event berbasis siklus (Cycle-Scheduled Event Queue):
- Scheduled Interrupt Line: Jadwalkan interrupt pada siklus pasti. Contoh: NMI dijadwalkan tepat pada
cycle = 29658. - Mock Memory-Mapped I/O: Ganti periferal input interaktif (seperti joypad/controller register) dengan predefined replay script. Jangan membaca event keyboard host OS secara langsung dalam regression harness.
- Float Bus Reads: Bila CPU membaca alamat open-bus, kembalikan nilai deterministik (misalnya bit lsb/msb dari PC sebelumnya atau byte terakhir yang ada di data latch).
Integrasi Pipeline CI (GitHub Actions)
Jalankan regression test pada environment headless tanpa dependensi grafis (SDL, OpenGL) atau audio driver (ALSA, PulseAudio). Pisahkan build target core engine murni sebagai shared library atau static CLI runner.
name: Cycle Verification
on: [push, pull_request]
jobs:
emulator-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Compile Headless Test Harness
run: |
cmake -B build -DHEADLESS_TEST_MODE=ON
cmake --build build --target cpu_regression_runner
- name: Run Deterministic ROM Tests
run: |
./build/cpu_regression_runner tests/roms/cpu_instr_timing.bin --log golden/cpu_timing.logTips: Simpan artefak trace perbandingan hanya ketika terjadi assertion failure. Menyimpan file log tracing megabyte berukuran penuh pada setiap CI run yang lolos akan memperlambat I/O container CI secara drastis.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!