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 (misalnyadt = 20msuntuk tick 50Hz). Simulasi tidak boleh membacaQueryPerformanceCounteratau 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
NullRendererdanNullAudioyang 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-mathdan-ffp-contract=offpada GCC/Clang, atau/fp:precisepada 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):
- Kompilasi harness dengan flags strict precision (
-fno-fast-math -ffp-contract=off). - Jalankan binary harness melewati utility time/leak sanitizer (seperti Valgrind atau ASan) untuk mendeteksi uninitialized memory read yang berpotensi merusak hash.
- Gagalkan (exit code != 0) pipeline jika terjadi deviasi hash, tandai commit tersebut menyebabkan regression pada state simulasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!