urt-rs / docs /research /QVM_USERCMD.md
nmarshall's picture
Analyze QVM think routing inactivity and mode gates
0113c21
|
Raw History Blame Contribute Delete
7.73 kB

Native usercmd retrieval and game-QVM time prefix

RE-405–407 / F-163–165. qvm-usercmd.json retains verified Linux ELF64 retrieval graphs/trap cases and shipped qagame instruction evidence. Reproduce with python3 scripts/analyze_qvm_usercmd.py; verify with python3 scripts/test_qvm_usercmd.py.

This extends server acceptance and selector 7. Original executables, native callees and the complete QVM never execute. VM_ArgPtr results and cross-address-space mapping are supplied. Native retrieval instructions copy bytes, which are explicitly mapped into the synthetic QVM word memory; this is a bounded composition, not a running engine/VM bridge. Calls in QVM entry/wrapper slices are recorded. The time prefix stops before remaining simulation.

QCMD-001 — Trap 36, index guards and native record copy (RE-405)

The file-backed SV_GameSystemCalls switch slot 36 resolves to dedicated 0x40b7c8 / client-engine 0x473738. The report verifies the slot's little-endian pointer against the source file and retains its file offset and case instructions. This case first converts argument slot 2 through VM_ArgPtr, then treats slot 1's low 32 bits as a signed client index.

An index below zero or at least the signed sv_maxclients integer reaches Com_Error(1, format, index). The analysis stops at that call and does not assume normal continuation; the native graph's syntactic fallthrough is not an error-recovery contract. Pointer conversion occurs before this guard even on the invalid-index paths. Translation validity, pointer masking/bounds and actual error unwinding remain open.

Valid paths read the client-array base from svs+0x10, multiply the sign-extended index by 0x2df40, and copy three 64-bit words from client +0x2081c/+0x20824/+0x2082c to the converted destination. The trap case sets return EAX to zero before its common epilogue jump. The standalone SV_GetUsercmd performs the same signed guards, explicit stride multiplication and 24-byte copy; it is not called by the inlined trap case.

This independently confirms the earlier arithmetic candidate stride as an explicit access stride in these two native paths. Actual array allocation, valid slot lifetime and maximum-client policy still require recovery.

112 probes total (28 standalone and 28 trap cases per engine) cover indices -1/0/1/7/63/64/0x7fffffff against maxima 0/1/8/64. Independent tests verify guards, conversion requests, invalid destination retention, exact 24-byte copies, canaries and valid trap return zero. No Com_Error or VM_ArgPtr callee executes.

QCMD-002 — Selector 7 wrapper and command placement (RE-406)

The shipped qagame entry dispatch reads jump-table word at VM address 3564 and selects instruction 65 for selector 7. The case loads incoming local 48, forwards it in argument slot 8 and calls helper 155718, then returns zero. This matches the native selector/client-index boundary recovered previously; the local/table mapping is checked against the source QVM and existing ABI report.

Helper 155718 computes entity = 34091368 + 880*index, loads its client pointer from entity +520, and records trap target -37 (trap 36) with (index, client_pointer+472). Thus the native 24-byte stored usercmd is fetched into the game client record at +472. Entity indexing and pointer validity are supplied, not checked by this wrapper slice.

After retrieval, the helper writes global time word 34992532 into client +4044. It calls helper 154089 with the entity pointer only when (entity[+424] & 8) == 0 and global word 34084868 == 0. Bit-8 and global-gate producer semantics remain open; names such as bot/synchronous-client mode require independent confirmation. Retrieval and callback-time storage happen even when those gates suppress the think call.

108 wrapper compositions (54 per engine) cover indices 0/1/63, flag words 0/8/9, gate words 0/1/-1 and time words 1000/0xffffffff. Native copy results become six QVM words under explicitly supplied address mapping. Independent checks verify trap arguments, every copied word, metadata time and exact think-call gating. Native client storage and QVM game-client storage have different bases/layouts; they must remain separate in a Rust port.

QCMD-003 — Game-side time clamp, elapsed gate and alignment (RE-407)

Helper 154089 begins with a 436-byte QVM frame. The analyzed prefix (154090 through boundary 154210) loads entity +520 and requires client[+468] ==2; other sampled states jump to the return tail at 155716 without this prefix's time mutations.

For state 2, observed operations are sequential:

  1. Write game time 34992532 to client +6172.
  2. Preserve the raw command time from client +472 at client +6176.
  3. Clamp command time downward when it exceeds signed32(game_time+200), then upward when it is below signed32(game_time-1000).
  4. Compute signed32(command_time-client[+0]) as elapsed time.
  5. If elapsed <1 and client[+3108] !=2, return after the preceding time stores/clamp.
  6. Cap elapsed at 200. This modifies local 328, independently of the command timestamp.
  7. If client[+712] is nonzero, set command time to signed32(trunc_toward_zero(signed32(command_time+7)/8)*8).

The eight-unit adjustment occurs after elapsed calculation and its cap; this prefix does not recompute elapsed or reapply the clamp after rounding. For ordinary positive times this rounds upward to a multiple of eight. Negative/boundary inputs require the recovered signed/truncating expression rather than a generic ceiling formula. Client +0 is provisionally the player-state command time; the +3108 mode and +712 alignment flag still need producer/consumer confirmation.

147 probes cover three rejecting states, 128 ordinary combinations of command time/prior time/mode/alignment, four negative-time rounding fixtures, and twelve signed-boundary combinations near 0x7fffffff/0x80000000. Independent modular arithmetic predicts command time, raw/observed time stores, elapsed and early return. Overflow in the game-time bounds can produce unusual sequential-clamp behavior in synthetic boundary fixtures. These are modeled instruction results, not evidence of legitimate clock wrapping or runtime parity.

The evaluator adds an optional explicit stop-target boundary, allowing state guards and the continuing prefix to stop before the remaining helper. Existing callers keep their original behavior/output. A focused test proves that an unsupported boundary instruction is not executed and still fails when the optional stop is absent.

Validation and remaining work

Eight tests verify native signed guards/pointer ordering/copy retention, file-backed trap slot and explicit stride, selector/table/argument mapping, wrapper copy/metadata/gates, game-side signed clamps/elapsed/mode/alignment, paired results, source identities and optional stop behavior.

RE-404 remains open for complete callback/producer/allocation/runtime work. RE-408 must confirm the global/record flag meanings, recover command generation and all timestamp/elapsed consumers, continue helper154089 into movement/simulation, characterize selector7 trap/VM mutation effects and address translation, audit other trap36 callers, compose packets through native acceptance/retrieval/game-side processing and validate real traffic/all-platform behavior. Full entity/player/snapshot codecs and Rust/Bevy/Solari implementation remain open.

Follow-up: think gates continues the timestamp prefix through special routing, command suppression, inactivity decisions and mode-word selection. Gravity/speed, complete movement inputs, action effects and simulation/runtime validation remain open.