Skip to content

Commit 1aca584

Browse files
committed
Replace BTech Mersenne Twister with xoshiro
1 parent 9242ef6 commit 1aca584

8 files changed

Lines changed: 204 additions & 174 deletions

File tree

CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,20 @@ if(BUILD_TESTING)
298298
"${CMAKE_SOURCE_DIR}/src")
299299
add_test(NAME btech_parts COMMAND btech_parts_test)
300300

301+
add_executable(btech_random_test tests/btech_random.c
302+
${BTECH_SOURCE_DIR}/core/random_xoshiro.c
303+
${BTECH_SOURCE_DIR}/ui/mech_stat.c)
304+
target_compile_definitions(btech_random_test PRIVATE BTECH_INTERNAL=1)
305+
target_compile_options(btech_random_test PRIVATE -ffunction-sections)
306+
target_link_options(btech_random_test PRIVATE -Wl,--gc-sections)
307+
target_link_libraries(btech_random_test PRIVATE btmux_options)
308+
target_include_directories(btech_random_test PRIVATE
309+
"${CMAKE_BINARY_DIR}/${BTECH_SOURCE_DIR}"
310+
"${CMAKE_BINARY_DIR}"
311+
${BTECH_PRIVATE_INCLUDE_DIRS}
312+
"${CMAKE_SOURCE_DIR}/src")
313+
add_test(NAME btech_random COMMAND btech_random_test)
314+
301315
add_executable(btech_persistence_schema_test
302316
tests/btech_persistence_schema.c
303317
${BTECH_SOURCE_DIR}/persistence/schema.c)

docs/content/en/docs/concepts/btech-architecture.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Each domain owns both its state and the operations that change that state:
1515

1616
| Domain | Primary ownership |
1717
| --- | --- |
18-
| `core` | Runtime context, random generator, events, and heartbeat |
18+
| `core` | Runtime context, xoshiro256** random generator, events, and heartbeat |
1919
| `special` | Native special-object registry and typed object operations |
2020
| `map` | Battle maps, terrain, map objects, and cached LOS state |
2121
| `unit` | Mechs, templates, parts, sections, critical slots, and weapons |
@@ -31,6 +31,9 @@ Each domain owns both its state and the operations that change that state:
3131
| `persistence` | SQLite schema and domain persistence adapters |
3232
| `integration` | Narrow adapters to MUX-owned services |
3333

34+
The context-owned gameplay generator is xoshiro256**, seeded once from Linux
35+
OS entropy during BTech startup. Its runtime state is not persisted.
36+
3437
Concrete `Mech`, `BattleMap`, `Autopilot`, and runtime-context layouts are
3538
private. Cross-domain interfaces use forward declarations, database object
3639
references, or domain operations rather than copying another domain's state.

src/btech/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ set(BTECH_SOURCES
9090
core/context.c
9191
core/heartbeat.c
9292
core/legacy_events.c
93-
core/random_mt.c
93+
core/random_xoshiro.c
9494
economy/econ.c
9595
economy/econ_cmds.c
9696
economy/unit_cost.c

src/btech/core/random.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <stdbool.h>
4+
#include <stdint.h>
45

56
typedef struct BtechRollStatistics {
67
int rolls[11];
@@ -11,15 +12,15 @@ typedef struct BtechRollStatistics {
1112
int total_critical_rolls;
1213
} BtechRollStatistics;
1314

14-
enum { BTECH_RANDOM_STATE_SIZE = 624 };
15+
enum { BTECH_RANDOM_STATE_SIZE = 4 };
1516

1617
typedef struct BtechRandom {
17-
unsigned long state[BTECH_RANDOM_STATE_SIZE];
18-
int index;
18+
uint64_t state[BTECH_RANDOM_STATE_SIZE];
1919
BtechRollStatistics statistics;
2020
bool initialized;
2121
} BtechRandom;
2222

23-
void btech_random_seed(BtechRandom *random, unsigned long seed);
24-
unsigned long btech_random_u32(BtechRandom *random);
23+
void btech_random_seed(BtechRandom *random, uint64_t seed);
24+
bool btech_random_seed_from_system(BtechRandom *random);
25+
uint64_t btech_random_u64(BtechRandom *random);
2526
long btech_random_i31(BtechRandom *random);

src/btech/core/random_mt.c

Lines changed: 0 additions & 120 deletions
This file was deleted.

src/btech/core/random_xoshiro.c

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "random.h"
2+
3+
#include <assert.h>
4+
#include <errno.h>
5+
#include <stddef.h>
6+
#include <sys/random.h>
7+
8+
static uint64_t rotate_left(uint64_t value, int count) {
9+
return (value << count) | (value >> (64 - count));
10+
}
11+
12+
static uint64_t splitmix64_next(uint64_t *state) {
13+
uint64_t value = (*state += UINT64_C(0x9e3779b97f4a7c15));
14+
15+
value = (value ^ (value >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
16+
value = (value ^ (value >> 27)) * UINT64_C(0x94d049bb133111eb);
17+
return value ^ (value >> 31);
18+
}
19+
20+
void btech_random_seed(BtechRandom *random, uint64_t seed) {
21+
assert(random != nullptr);
22+
23+
for (size_t index = 0; index < BTECH_RANDOM_STATE_SIZE; index++) {
24+
random->state[index] = splitmix64_next(&seed);
25+
}
26+
random->initialized = true;
27+
}
28+
29+
bool btech_random_seed_from_system(BtechRandom *random) {
30+
uint64_t seed;
31+
size_t offset = 0;
32+
33+
assert(random != nullptr);
34+
35+
while (offset < sizeof(seed)) {
36+
ssize_t bytes = getrandom((char *)&seed + offset, sizeof(seed) - offset, 0);
37+
38+
if (bytes > 0) {
39+
offset += (size_t)bytes;
40+
continue;
41+
}
42+
if (bytes < 0 && errno == EINTR) {
43+
continue;
44+
}
45+
return false;
46+
}
47+
48+
btech_random_seed(random, seed);
49+
return true;
50+
}
51+
52+
uint64_t btech_random_u64(BtechRandom *random) {
53+
uint64_t result;
54+
uint64_t temporary;
55+
56+
assert(random != nullptr);
57+
assert(random->initialized);
58+
59+
result = rotate_left(random->state[1] * UINT64_C(5), 7) * UINT64_C(9);
60+
temporary = random->state[1] << 17;
61+
62+
random->state[2] ^= random->state[0];
63+
random->state[3] ^= random->state[1];
64+
random->state[1] ^= random->state[2];
65+
random->state[0] ^= random->state[3];
66+
random->state[2] ^= temporary;
67+
random->state[3] = rotate_left(random->state[3], 45);
68+
69+
return result;
70+
}
71+
72+
long btech_random_i31(BtechRandom *random) {
73+
return (long)(btech_random_u64(random) >> 33);
74+
}

src/btech/ui/mech_stat.c

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
/* Make statistics 'bout what we do.. whatever it is we _do_ */
1414

1515
#include <assert.h>
16-
#include <time.h>
16+
#include <stdint.h>
17+
#include <stdio.h>
18+
#include <stdlib.h>
1719

1820
#include "btech/context.h"
1921
#include "command_handlers_api.h"
@@ -26,7 +28,10 @@
2628
#include "registry_api.h"
2729

2830
void init_stat(BtechContext *context) {
29-
btech_random_seed(&context->random, (unsigned long)time(nullptr));
31+
if (!btech_random_seed_from_system(&context->random)) {
32+
perror("getrandom");
33+
exit(EXIT_FAILURE);
34+
}
3035
}
3136

3237
static const int chances[11] = {1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1};
@@ -67,62 +72,31 @@ void do_show_stat(CommandInvocation *invocation) {
6772
/*
6873
* Returns an integer chosen randomly from the interval [low,high].
6974
*
70-
* To eliminate bias from rounding error, this routine repeatedly takes some
71-
* number of high order bits from the Mersenne Twister, until it finds a value
72-
* <= (high - low). If we take n bits, such that 2^n is the smallest power of
73-
* two greater than (high - low), then this procedure should only require
74-
* another iteration 50% or less of the time. (The actual value would be
75-
* (2^n - (high - low)) / (high - low).) It also always terminates due to the
76-
* statistical qualities of the Mersenne Twister, although possibly only after
77-
* several (but generally very few) iterations.
78-
*
79-
* For example, computing a D6 should require a second iteration 33% (1/3rd) of
80-
* the time, a third iteration 11% (1/9th) of the time, a fourth iteration 3.7%
81-
* (1/27th) of the time, a fifth iteration 1.2% of the time (1/81st) of the
82-
* time, a sixth iteration 0.4% (1/243rd) of the time, and so on. Or in other
83-
* words, this will require fewer than six iterations 99.6% of the time, while
84-
* completely eliminating rounding bias.
75+
* To eliminate modulo bias, this routine repeatedly draws from xoshiro256**
76+
* until it finds a value in the largest multiple of the interval width that
77+
* fits in a uint64_t. This requires at most one additional draw on average.
8578
*
8679
* This code is on the critical path, but modern processors can compute this
8780
* stuff really fast. There's really no need to have the compiler inline it to
8881
* perform further optimization.
8982
*/
9083
long btech_random_range(BtechContext *context, long low, long high) {
91-
const unsigned long int range = (unsigned long int)(high - low);
92-
93-
unsigned long value;
94-
unsigned int nn;
84+
uint64_t width;
85+
uint64_t limit;
86+
uint64_t value;
9587

88+
assert(context != nullptr);
9689
assert(high >= low);
9790

98-
/*
99-
* Compute n, the shift value. We're using the 32-bit version of the
100-
* Mersenne Twister, so we only need shifts up to 32. (If we did need a
101-
* larger value, we would also need to expand our random number size.)
102-
*
103-
* We can special case some of the common values (such as n = 8 for
104-
* range = 5, for the D6) if this loop becomes a concern.
105-
*/
106-
for (nn = 0; nn < 32; nn++) {
107-
if ((range >> nn) == 0)
108-
break;
91+
width = (uint64_t)high - (uint64_t)low + UINT64_C(1);
92+
if (width == 0) {
93+
return (long)btech_random_u64(&context->random);
10994
}
11095

111-
nn = 32 - nn;
112-
113-
/* Shifts >= bit width are undefined in C. At least on x86, they
114-
* apparently do nothing, which causes the following do-while loop to
115-
* run until the generator returns 0. */
116-
if (nn == 32) {
117-
return 0;
118-
}
119-
120-
assert(nn < 32);
121-
122-
/* Repeatedly select random numbers until we get an acceptable one. */
96+
limit = UINT64_MAX - UINT64_MAX % width;
12397
do {
124-
value = btech_random_u32(&context->random) >> nn;
125-
} while (value > range);
98+
value = btech_random_u64(&context->random);
99+
} while (value >= limit);
126100

127-
return low + value;
101+
return (long)((uint64_t)low + value % width);
128102
}

0 commit comments

Comments
 (0)