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:10

Perbandingan 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.log

Tips: 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.