Docs · Engine notes
Engine map (Victoria 3 1.15.1)
What is known about the engine: tick, threads, memory, rendering, saves, VFS.
Copied from docs/engine-map.md in the V3X repository, file dated 2026-11-13.
A one-page summary of what has been reverse-engineered, with links to the detailed notes in
docs/re/. All addresses are RVAs in victoria3.exe 1.15.1 (PE TimeDateStamp 0x6af3094f,
SizeOfImage 0x60fb000, game_build_id() = 6af3094f-60fb000). They will move with the next
patch: use the anchor in the “found by” column, never the number.
The engine is Paradox’s Clausewitz + Jomini, one static exe (static CRT, mimalloc 2.0.x, spdlog,
PhysFS 3, SDL, D3D11/Vulkan renderers), no symbols, with MSVC RTTI and .pdata for every
non-leaf function.
Process start
| step | what | where |
|---|---|---|
| 1 | Windows loads binaries/version.dll (V3X) because the exe imports VERSION.dll |
src/proxy.c |
| 2 | V3X hooks the exe entry point; plugins load on the main thread before the CRT | src/core.c |
| 3 | CRT startup, static initializers (GUI functions, console commands and tick-task types register themselves here) | docs/re/gui.md, docs/re/console.md |
| 4 | WinMain: settings, renderer, VFS init, then the database/asset load (Empty -> Game takes ~170-200 s on the lead’s machine) | docs/re/loading.md |
Simulation tick (docs/re/simulation.md, docs/re/tick_par.md)
| RVA | role | found by |
|---|---|---|
| 01465690 | GameState tick, once per game tick | string Processing Tick: (single xref) |
| 014736c0 | tick-task runner: runs the due tasks strictly one after another | Tick Tasks for {} |
| 0146bce0 | due-list builder (filters the global order by rate) | runner’s callee, docs/re/tick_par.md |
| 0146d010 | RunTask (Prepare, Execute, range/context workers) | Context %d (exactly one xref) |
| 0146d0ee | virtual dispatch of a task’s Execute (vtable slot 6) | inside RunTask |
| 01468a60 / 01468d10 | RenderFrameIfNeeded guard / helper (frames during long ticks) |
gamestatetick.cpp:262 |
- 272
CTickTask_<Name>classes (RTTI). Rates TICK, DAY, WEEK, MONTH, QUARTER, YEAR; priorities DEFAULT / LATE. Order: one-time stable sort + dependency DFS (dependencies are names, order hints only, not a thread-safety contract). - Parallelism exists only inside a task, through range stages (
Stepflags bit0 = parallel range). Hot tasks: HandleAutonomousInvestment, UpdateAI, RecalculateModifierNodes, UpdatePopGrowth, UpdateStateTrades (an iterative solver whose evaluation is already parallel). - Script RNG scope: one global owner pointer
*0x540ae80(owner thread id at+0x10);ALLOW_RANDOM_IN_SCOPEsets it, parallel ranges swap it out. Script outside a tick task has no owner and crashes (docs/safety.md “Script and RNG scope”; check withsdk/v3x_game.h). - Saves run as session commands on a job, between ticks (autosave between the monthly pulse tick and the next tick).
Threads (docs/re/threading.md)
| RVA | role | found by |
|---|---|---|
| 03df8a50 | task scheduler init (Threading.TaskThreadCount, -1 = auto) |
Spawning {} normal worker threads |
| 03df9180 | worker thread entry | lea at 03dfa64c |
| 03dfac40 | priority per worker kind (BELOW_NORMAL / NORMAL / ABOVE_NORMAL) | 3x SetThreadPriority |
The game never sets affinity and is not P/E-core aware (counts physical cores with
GetLogicalProcessorInformation). Up to 64 workers per pool. cpu_sched places threads with CPU
sets.
Memory (docs/re/memory.md)
operator new= mimallocmi_new0x43d95c0 (statically linked, 15k call sites);mi_free0x43d94b0 (leaf, no.pdata);mi_malloc0x43d8c20. mimalloc v2.0.x (32 MiB segments). The allocator is already swapped; replacing it is pointless.- mimalloc options are a static table that can be written before CRT init (mem_tuner does this,
instead of
MIMALLOC_*env vars that leak to child processes and GPU drivers). - Measured: 17.5 GB private commit in a long session; about half outside mimalloc (likely the iGPU
driver’s resource backing). OOM:
bad_allochandlers mapped withtools/re_mem_catch.py.
Rendering (docs/re/graphics.md)
- Backends: DX11 and Vulkan (no DX12 game renderer; DX12 strings are SDL’s). Launch args
-renderer=dx11|vulkan,-dx11,-vulkan. - Present wrapper 0x3c7a8d0 (hook it rather than DXGI vtables). Frame cap is a busy-wait
(0x379a540,
pauseloop at 0x379a710) driven byowner+0x37/+0x38(1/fps);frame_pacerreplaces it with a high-resolution timer sleep. - Long ticks call
RenderFrameIfNeededso the UI keeps drawing during simulation. - XeSS/DLSS/FSR backends are not built into this Windows build.
Loading and assets (docs/re/loading.md, docs/re/streaming.md, docs/re/lazy_mesh.md, docs/re/mapmode.md)
- Phases: checksum (
Starting checksum calculation, 0377b120), localization, texture lookup, mesh types (IterateTypesThreaded0x3ae4d80,CMeshTypevtable 0x4ace220, Load = slot +0x30 0x3adcfd0: every declared mesh type is loaded eagerly), script database parse. - No generic resource manager: each asset kind has its own loader and cache. Textures:
GetOrLoadTexture 0x3c5b4f0, global cache map 0x5421820, CreateTexture 0x3c5af20; the engine’s
own texture streamer (
CGfxTextureStreamer, Start 0x3c613e0) is turned off by every quality preset (asset_stream forces it on). - The texture cache is weak: map modes and GUI views re-read textures from disk (map_cache measures and optionally pins them).
Saves (docs/re/enhance.md)
SaveGameState0x3662910, reached from autosave 0x0ca9580 throughExecuteGameCommand0x1085b90 on a session job. enhance hooks it to emitsave_begin/save_end.debug_modeforces text saves (156 MB vs 26 MB binary); serialization, not disk, is the ~3.2 s freeze. Writes go to<name>.v3.tmp, then rename.- Game speed table: 16 bytes in
.rdata0x4b08610.
Console (docs/re/console.md)
CConsole(vtable 0x4a90d88), Tab =CConsole::Autocomplete0x36be560; manager getter 0x3dc0e00, manager globalexe+0x5c72730, matcher 0x3dc2140.- Registry entries
SCommandData(0x210 bytes):+0xb0OnExecute,+0xb8argument completer,+0xc0its user pointer. ~300 commands, ~26 with completers (console_plus adds them). - The console is blocked in ironman by one predicate that references
No Console commands in ironman!(ironman_console clears only that reason).
Virtual file system and mods (docs/re/vfs.md, docs/re/modloading.md)
| RVA | role | found by |
|---|---|---|
| 0x3e17d10 | vfs_init | ../platform_specific_game_data |
| 0x3e17820 | MountData | No subdirs mounted for game dir from candidates: {} |
| 0x3dff0b0 | mount(unused, dir, mount_point): prepends (highest priority) | 45 33 C0 E8 call in MountData |
| 0x3e16bd0 | refresh the cached search path | in MountData |
| 0x3b6d500 | dlc.cpp: mounts one DLC / mod |
Search order: last mod … first mod, DLC, game, jomini, clausewitz. Every lookup probes every root
(PhysFS DIR archiver: GetFileAttributesExW, CreateFileW per root), which costs ~34 failed
syscall pairs per vanilla file with 22 mods + 12 DLC (mod_loader indexes roots). A mod whose
metadata fails to parse is dropped silently.
Logging (docs/re/logging.md)
spdlog >= 1.12 behind NLog::CCategoryLogger; categories and sinks come from
platform_specific_game_data/log_settings_live.json. Hook point: the virtual
spdlog::logger::sink_it_ (exe+0x43d70f0), found through the RTTI of spdlog::logger (vtable
slot 2). log_filter drops and dedupes there, before any file sink.
GUI (docs/re/gui.md)
GUI global functions are registered by name from CRT static initializers (e.g. OpenWikiUrl);
before GUI init they queue to a pending list. The main menu is frontend_main.gui
(mainmenu_panel_bottom); a file with the same VFS path in a content pack overrides it.
Country history graphs (docs/re/history.md)
Trends are cleared by 11 Trend::Clear calls when a country is demoted to AI (0x1465ef0);
keep_history NOPs them.
Where to start for common goals
| goal | start at |
|---|---|
| per-tick logic / “every day” callbacks | RunTask (all tasks) or one task’s Execute via RTTI (CTickTask_<Name> slot 6) |
| timing a phase of loading | load_transition / load_transition_begin events (fast_load) |
| before/after a save | save_begin / save_end events (enhance); never run script there |
| file overrides without a mod | a folder in binaries/v3x/content_packs/ (content_packs) |
| console features | register_command (V3X console) or the engine registry (docs/re/console.md) |
| rendering / frame timing | Present wrapper 0x3c7a8d0, frame cap 0x379a540 |
| memory | mimalloc entry points above; mem_tuner / alloc_tracker as examples |