Refaktor engine simulasi C++ legacy—seperti pada codebase Real Virtuality atau rilis source code Bohemia Interactive (CWR)—memiliki risiko tinggi merusak integrasi logika fisika, AI, dan transisi state tanpa disadari. Solusi pengujian manual memakan waktu dan tidak dapat diotomasi. Pendekatan yang andal adalah membangun test harness headless replay deterministik: mengeksekusi simulasi tanpa dependensi audio/visual di pipeline CI, memutar ulang rekaman input diskrit, dan memverifikasi keabsahan state memori menggunakan checkpoint hashing.

Pemisahan Core Simulation Loop dari Presentation Layer

Simulasi legacy umumnya mencampur kalkulasi state dengan rendering (DirectX/OpenGL) dan input polling OS (Win32 API) di dalam satu loop utama (while (!exit) { Update(); Render(); }). Arsitektur ini harus diisolasi agar dapat berjalan di lingkungan headless seperti container Linux di runner CI.

Langkah decoupling inti:

  • Fixed-Timestep Tick Loop: Ganti delta-time variabel (deltaTime = currentTime - lastTime) dengan interval waktu diskrit yang kaku (misalnya dt = 20ms untuk tick 50Hz). Simulasi tidak boleh membaca QueryPerformanceCounter atau clock sistem secara langsung.
  • Mocking Subsystem I/O: Abstraksikan rendering, audio, dan network input di balik interface murni (pure virtual interfaces). Pada mode replay headless, instansiasi backend berupa NullRenderer dan NullAudio yang memotong call GPU/driver.
  • Kompilasi Khusus Headless: Gunakan compiler flag (misal -DHEADLESS_DEDICATED) untuk menghilangkan kode drawing atau link library eksternal (DirectX/Vulkan) saat mengompilasi binary test harness.

Rekam-Replay Input Stream dan State Hashing

Secara matematis, engine deterministik dapat dimodelkan sebagai state machine:

State(t + 1) = Step(State(t), Input(t))

Jika State(0) dan deret Input(0 ... N) identik, maka State(N) harus selalu menghasilkan bit-level state yang sama.

Format Input Stream

Rekam input pemain per tick ke dalam buffer serial. Struktur input per frame harus bersifat raw, compact, dan membuang dependensi event handler windowing:

struct TickInput {
    uint32_t tick_number;
    uint16_t digital_actions; // Bitmask: bit 0 = forward, bit 1 = fire, dsb.
    int16_t  analog_yaw;       // Normalisasi input axis [-32768, 32767]
    int16_t  analog_pitch;
};

Verifikasi State via Hash Memory

Daripada membandingkan jutaan variabel individual, jalankan non-cryptographic hashing (misal FNV-1a, MurmurHash3, atau xxHash64) terhadap snapshot memori representasi state tiap K tick atau pada akhir eksekusi scenario.

Data yang masuk dalam hashing buffer:

  • Posisi dan orientasi entity (transform matrices / fixed vectors).
  • Velocity, angular velocity, dan state flag fisika.
  • Status variabel gameplay (health, ammo, active orders AI).
  • State internal Pseudo-Random Number Generator (PRNG).

Penyebab Flaky Test dan Desinkronisasi State

Ketidakcocokan hash (desync) pada simulasi berulang hampir selalu disebabkan oleh tiga faktor non-deterministik berikut:

1. Non-Determinisme Floating-Point (FPU/SIMD)

Optimalisasi compiler modern mengubah urutan asosiatif operasi float atau menggabungkan perkalian dan penjumlahan menjadi satu instruksi FMA (Fused Multiply-Add), yang mengubah presisi rounding bit akhir.

  • Compiler Flags: Matikan optimasi matematika agresif. Gunakan -fno-fast-math dan -ffp-contract=off pada GCC/Clang, atau /fp:precise pada MSVC. Jangan gunakan -Ofast.
  • Cross-Platform Drift: Arsitektur x86-64 (SSE2) versus ARM (NEON) memiliki handling NaN dan subnormal floating point berbeda. Replay test harness harus diverifikasi pada environment compiler dan arsitektur target yang identik, atau gunakan fixed-point arithmetic jika simulasi menuntut cross-platform replay determinism.

2. Unseeded atau Shared Global PRNG

Pemanggilan fungsi acak bawaan seperti std::rand() atau instans global std::mt19937 yang diakses lintas modul menyebabkan urutan angka acak berubah jika eksekusi task diubah. Setiap instance simulasi harus memiliki seed eksplisit dan state PRNG terisolasi yang di-reset bersamaan dengan checkpoint state.

3. Iterasi Pointer Container (ASLR Artifacts)

Menggunakan raw pointer sebagai key pada container seperti std::set<Entity*> atau std::unordered_map<Entity*, T> menyebabkan iterasi berjalan berdasarkan alamat memori (memory address). Karena ASLR (Address Space Layout Randomization) dan fragmentasi heap, alamat alokasi berbeda setiap eksekusi, merusak urutan eksekusi update logic.

Solusi: Gunakan stable integer ID (misalnya uint32_t EntityID) sebagai key komparasi pada struktur data, dan selalu urutkan entitas berdasarkan ID sebelum menjalankan simulation tick.

Implementasi Test Harness C++ Minimal

Implementasi minimal berikut mengonsumsi buffer input terstruktur, menjalankan loop simulasi diskrit tanpa subsistem visual, dan melakukan verifikasi hash integritas memori:

#include <cstdint>
#include <vector>
#include <cassert>
#include <cstring>

struct TickInput {
    uint32_t tick;
    float move_x;
    float move_y;
};

struct EntityState {
    uint32_t id;
    float x, y;
    float vx, vy;
};

// Hash FNV-1a 64-bit untuk hashing state buffer
uint64_t fnv1a_64(const void* data, size_t bytes, uint64_t hash = 14695981039346656037ULL) {
    const auto* ptr = static_cast<const uint8_t*>(data);
    for (size_t i = 0; i < bytes; ++i) {
        hash ^= ptr[i];
        hash *= 1099511628211ULL;
    }
    return hash;
}

class HeadlessSimWorld {
public:
    EntityState player{1, 0.0f, 0.0f, 0.0f, 0.0f};
    uint32_t current_tick = 0;
    const float fixed_dt = 0.02f; // 50 Hz

    void Step(const TickInput& input) {
        assert(input.tick == current_tick);
        
        // Update velocity berdasarkan input diskrit
        player.vx = input.move_x * 5.0f;
        player.vy = input.move_y * 5.0f;
        
        // Integrasi posisi sederhana (Euler)
        player.x += player.vx * fixed_dt;
        player.y += player.vy * fixed_dt;
        
        current_tick++;
    }

    uint64_t ComputeStateHash() const {
        // ponytail: struct padding diasumsikan nol; gunakan packing eksplisit jika di-cross-build.
        return fnv1a_64(&player, sizeof(EntityState));
    }
};

// Test runner
int main() {
    // 1. Setup rekaman input diskrit (fixture)
    std::vector<TickInput> input_stream = {
        {0, 1.0f, 0.0f},
        {1, 1.0f, 0.0f},
        {2, 0.0f, 1.0f},
        {3, 0.0f, 1.0f},
        {4, 0.0f, 0.0f}
    };

    // 2. Target hash yang terekam dari baseline build yang stabil
    const uint64_t EXPECTED_HASH_TICK_5 = 0xa4a2b97c2718e874ULL; 

    // 3. Jalankan headless loop
    HeadlessSimWorld world;
    for (const auto& input : input_stream) {
        world.Step(input);
    }

    // 4. Assertion hash state
    uint64_t final_hash = world.ComputeStateHash();
    assert(final_hash == EXPECTED_HASH_TICK_5 && "Simulation desync detected!");

    return 0;
}
[code] → skipped: Serialization file I/O & multi-entity spatial partitioning, add when pipeline data ingestion butuh load binary file scenario dari disk.

Integrasi Pipeline CI

Tambahkan target binary harness ke dalam CMake/Meson sebagai target standalone test. Di pipeline CI (misal GitHub Actions atau GitLab CI):

  1. Kompilasi harness dengan flags strict precision (-fno-fast-math -ffp-contract=off).
  2. Jalankan binary harness melewati utility time/leak sanitizer (seperti Valgrind atau ASan) untuk mendeteksi uninitialized memory read yang berpotensi merusak hash.
  3. Gagalkan (exit code != 0) pipeline jika terjadi deviasi hash, tandai commit tersebut menyebabkan regression pada state simulasi.