Skip to content

Ludus Randomness Architecture

Status: Core primitives and the first game consumer implemented; broader integration remains proposed. apps/world_demo scatters static placements per room from a recorded session seed and the checked-in manifest, harvests a placement of one authored layer, and regrows it with an advancing ordinal (milestone 1 is delivered across its three pull requests). Deferred to later milestones: the checkpoint state codec (milestone 2), weighted selection and automatic seed acquisition. FoundationMath ships addressed_random.hpp: checked domain-key derivation and address construction, Philox4x32-10 raw/block/float samples, and bounded sampling with a prepared-bound option. Existing PCG32 v1 behavior remains authoritative in the math specification. The implementation adds reference-answer, boundary, rejection-budget, allocation, worker-order, installed-SDK, and pinned WebAssembly corpus checks. FoundationMath's shader source math.slang carries a bit-exact port of the addressed raw, float and bounded samplers for kernels; the renderer's GPU preview path tracer uses it, and a GPU test checks it against the pinned known answers and the CPU reference (see GPU addressed sampling). weighted_reservoir.hpp and its shader port provide the sequential weighted reservoir for resampled light selection. random_domains.hpp checks domain manifests against the game and engine ID ranges at compile time, and the headless procedural scatter kernel is the first consumer outside the renderer (milestone 1). It makes no throughput or statistical certification claim. Sequential seed factories, wire state codecs, probability/weighted distributions, game-world integration, designer policies, and security entropy remain future work.

The specification below describes the complete target architecture. Code uses TryMakeRandomKey, TryMakeRandomAddress, SampleUInt32, SampleFloat01, TrySampleBlock, TryPrepareBound32, TrySampleBounded, TryAddReservoirCandidate, and TryComputeReservoirContributionWeight; illustrative API names elsewhere in this proposal are not additional shipped entry points.

Initial design, before the Gems review

Use two explicit ways to obtain simulation randomness: a caller-owned PCG32 stream for ordered work, and a pure Philox4x32-10 function addressed by logical event identity for parallel or independently regenerated work. Keep sampling functions small and versioned. World/game code owns identities, stream lifetime, replay metadata, and designer policies. Platform owns operating-system entropy. There is no global RNG service or initialization dependency in FoundationMath.

The baseline decisions are:

  • Preserve RandomStream, RandomState, PCG32 XSH-RR seeding, bounded mapping, float conversion, and state version 1 exactly.
  • Add addressable samples only for work whose results must survive job reordering, such as procedural chunks and independent gameplay events.
  • Derive keys from explicit world seed and stable numeric domain IDs; use logical object/event IDs, never thread IDs, pointers, or iteration order.
  • Separate raw bits, uniform distributions, designer policies, and security.
  • Make sampling allocation-free, exception-free, and noexcept; use explicit statuses for invalid inputs and exhausted address budgets.
  • Treat generator, derivation, mapping, and content versions as compatibility data. Randomness alone does not make floating-point gameplay deterministic.
  • Establish scalar reference answers before any SIMD/GPU optimization, and choose optimizations by representative end-to-end measurements.

The Gems review records the subsequent changes: versioned procedural recipes, read-only probability models for planning, distribution profiles with honest names, explicit presentation isolation tests, immutable weighted tables, and in-kernel GPU generation. The final proposal follows.

Decision and scope

The best default for Ludus is a small deterministic vocabulary with explicit ownership. Keep the working PCG32 implementation and add one counter-based algorithm when a consumer requires order-independent samples. Avoid a pluggable generator framework, virtual distribution objects, thread-local generators, background refill pools, or an engine-wide registry on the draw path.

Need Selected mechanism Owner
Existing sequential gameplay, shuffled decks PCG32 XSH-RR v1 RandomStream World subsystem or persistent component
Parallel events, reproducible chunks, independent entity choices Philox4x32-10 addressed samples Caller supplies immutable key and logical address
Bounded integers, probabilities, geometry samples Named, specified sampling functions FoundationMath; caller supplies source
Loot protection, shuffle bags, decision persistence Explicit policy plus saved policy state Game/domain code
Path-tracing convergence, temporal visual quality Renderer sampler: stratification, Sobol, blue noise as needed Renderer
New nondeterministic world seed, security bytes Checked OS/browser cryptographic byte source Platform, called outside simulation sampling

PCG32 is a compatibility and maintenance choice, not a claim that it wins every benchmark. Philox is selected for random access and parallel composition, not because ten multiplication rounds necessarily beat PCG for a single scalar draw. No Ludus throughput result is available yet.

Non-goals for the initial delivery: cryptographic PRNG implementation, universal distribution catalog, guaranteed cross-platform deterministic physics, automatic stream allocation, generation of globally unique IDs from random bits, and bit-exact CPU/GPU transcendental functions.

Layering and implementation shape

flowchart TD
    Base[FoundationBase: types and assertions] --> Math[FoundationMath: raw bits and sampling]
    Math --> Sequential[Caller-owned PCG32 streams]
    Math --> Addressed[Immutable Philox keys and logical addresses]
    Sequential --> World[World/game: identities, policies, checkpoints]
    Addressed --> World
    Addressed --> Render[Renderer: independent visual sampling]
    Entropy[Platform: checked secure bytes] --> Setup[Application: select and record world seed]
    Setup --> World
    World --> Tools[Runtime/tools: tracing, replay, tuning]

Extend Ludus::FoundationMath; do not create a separate FoundationRandom module or lifecycle-managed RandomSystem. The existing opt-in random.hpp remains small. The shipped addressed_random.hpp provides key/address value types and non-template Philox functions. Add random_sampling.hpp only when the first additional sampler ships. Implement algorithms in corresponding .cpp files. Private helpers live under src/internal/; no third-party type leaks into installed headers. Base never includes any of these headers.

All hot operations are allocation-free and noexcept. Invalid arguments return MathStatus and leave outputs unchanged. Validate before advancing a stream. Fallible multi-draw additions compute with a local stream copy, then commit the new stream and result together on success. A failed transaction therefore leaves the original stream unchanged. Existing unbounded v1 bounded sampling keeps its current contract. FoundationMath calls no logger, entropy API, job system, or diagnostic callback during a draw.

For multi-element mutation such as shuffling, validate the entire operation first; use a subsequent path that cannot return a partial failure. A capped rejection variant would need staging storage or an explicit partial-progress contract; do not imply that copying the stream makes array writes transactional.

Use the project's fixed-width aliases, unsigned arithmetic, explicit narrowing, lightweight self-sufficient headers, and normal scalar alignment. No public <random>, heavy STL machinery, virtual interfaces, forced-inlining policy, or compiler-specific 128-bit integer requirement. Tiny source adapters can remain private helpers instead of exposing a templated public distribution framework.

Preserve the existing sequential contract

RandomStream remains PCG32 XSH-RR: 64-bit state, odd 64-bit increment, multiplier 6364136223846793005, and output from the old state. Preserve the two-step reference seeding, selector limit 0x7fffffffffffffff, deterministic default seed/selector zero, rejection-modulo bounded sampling, high-24-bit float conversion, and state version 1. The existing six-word fixture for seed 42, selector 54 remains unchanged. These conventions match the PCG reference API.

Save each mutable stream with its logical owner. One loot subsystem can own one loot stream; an entity may own a stream if it genuinely needs a sequential random process. Do not add 16 bytes to every entity merely because some entities use randomness. World structure owns placement and lifetime, not FoundationMath.

Each stream has one mutable owner at a time. Pass it by reference to ordered code. Sharing under a lock prevents a data race but still makes the sequence depend on lock acquisition order. Worker index, job number, thread ID, pointer, wall-clock time, and unordered-container traversal cannot establish simulation identity. A worker-local scratch generator is acceptable only when its sequence is deliberately irrelevant to simulation and persistent content.

Copying a stream deliberately copies its future sequence; it does not split it. Use copies for rollback, tests, and transactional sampling. Introduce no generic Fork() that suggests independent child streams. Distinct PCG selectors and mixed seeds are useful organizational tools; they do not prove mathematical independence or prohibit repeated output words. PCG's period is not a license to consume arbitrarily large simulation corpora without testing the actual use.

Stable keys, domains, and identities

Application/world setup records a uint64 root seed. Zero is a valid deterministic seed. Automatically seeded sessions obtain it once from Platform and immediately record it; simulation never refreshes it opportunistically.

Each independent meaning has a stable nonzero uint32 domain ID in a checked-in game/content manifest: loot selection, loot affixes, combat spread, AI decisions, terrain, and cosmetic particles are example domains. IDs are explicitly assigned, unique within the manifest, and never recycled. Labels are tooling metadata. Rename a label without changing the ID. Validate duplicates when building content or loading the manifest; draw calls do no string hashing or registry lookup. Different games may maintain different manifests.

Game and engine domains use separate ranges so that neither needs a shared registry. Game manifests assign IDs in [1, 0x00FFFFFF]. Engine-owned domains use IDs from 0x01000000, conventionally four ASCII characters such as the renderer's BAKE (0x42414B45) and RT01 (0x52543031). FoundationMath's random_domains.hpp provides CheckRandomDomains, a constexpr check that every entry of a manifest is nonzero, inside its owner's range and unique. A checked-in manifest asserts it with static_assert, so a duplicate or out-of-range ID fails the build rather than a draw.

The proposed derivation v1 is deliberately small:

W = 0x9e3779b97f4a7c15
Mix64(x):
    x = (x xor (x >> 30)) * 0xbf58476d1ce4e5b9
    x = (x xor (x >> 27)) * 0x94d049bb133111eb
    return x xor (x >> 31)

domainKey = Mix64(rootSeed + (uint64(domainId) + 1) * W)

Arithmetic wraps modulo 2^64. This uses the SplitMix64 finalizer, not a mutable SplitMix generator or a cryptographic KDF. For a fixed root seed, the odd multiplier and reversible finalizer give distinct keys to distinct domain IDs; this structural statement is not a statistical-independence guarantee. The root/domain combination cannot be injective across all worlds in a 64-bit key, and no global uniqueness promise is made. Cache the key with the domain owner.

For a new sequential stream, derive seed = Mix64(domainKey + scopeId) and use selector = uint64(domainId), then call the existing seeding function. This is a new named setup convention; do not replace an existing game's explicit seed scheme. Different scopes in one domain have the same increment and different starting phases. Use the addressed path when independent parallel identity is the requirement; do not advertise these phases as guaranteed disjoint streams.

scopeId is a stable uint64 identity assigned by gameplay/content. It must survive reconstruction: a saved entity creation ID, persistent room ID, or a collision-checked cooked chunk index. Runtime entity handles and array positions are unsuitable. Coordinates must be packed injectively within documented bounds or mapped through a persistent catalog; a general coordinate hash cannot prove unique scope identity. Preserve the ID allocator state and failed-creation semantics across rollback so IDs are not reused accidentally.

An entity respawn gets a new creation identity unless repeating its original random behavior is explicitly intended. Independent worlds have separate root seed contexts. Preview tools may deliberately reuse a key/address to reproduce an outcome, but their mutable policy state belongs to a separate preview world.

Addressed randomness for parallel work

Add RandomKey containing the derived 64-bit key and RandomAddress containing:

Field Width Meaning
Scope uint64 Stable entity, room, chunk, or event-owner identity
Event uint32 Owner-local persisted event ordinal
Dimension uint16 Stable sample meaning within that event

For example, one attack's dimensions can be horizontal spread, vertical spread, and damage variation. Assign dimensions explicitly within each domain/event schema; adding a sample uses an unused dimension or a new domain. Never insert an implicit next-dimension call that renumbers subsequent meanings. A helper requiring several samples reserves a documented range and checks that it fits.

An event counter advances exactly once when the simulation commits a new logical event, independent of whether a sample succeeds or a visual effect renders. Retried evaluation uses the same event ID. An aborted tick follows the world's transaction/abort policy; RNG code does not publish a partially completed event as successful. Checkpoint event counters and identity allocators with world state.

Use Philox4x32-10 with the standard Random123 constants and output ordering. For an internal rejection attempt a in [0,65535], pack its counter and key as:

counter[0] = low32(Scope)
counter[1] = high32(Scope)
counter[2] = Event
counter[3] = uint32(Dimension >> 2) | (uint32(a) << 16)
key[0]     = low32(domainKey)
key[1]     = high32(domainKey)
outputLane = Dimension & 3

Dimensions 0-3 share one block with different lanes; dimensions 4-7 share the next. The two unused bits in the low half of counter[3] stay zero in this schema. Different supported (Scope,Event,Dimension,attempt) addresses map to different counter/lane positions. This prevents accidental address aliasing; it does not prevent equal 32-bit output values or establish cryptographic security.

Raw SampleUInt32(key,address) uses attempt zero. SampleFloat01 uses the same raw word and the existing high-24-bit mapping. TrySampleBlock requires a dimension divisible by four and returns four words for that aligned group. There is no hidden cached block, mutable cursor, or attempt field exposed for general callers to change. Sampling two different distributions at the same address intentionally reuses random input; ordinary independent choices require separate dimensions or domains.

TrySampleBounded starts at attempt zero and changes only the internal attempt on rejection. It cannot perturb another event or dimension. After all 65,536 attempts fail, return MathStatus::OutOfRange with output unchanged; do not wrap the attempt, reduce modulo without rejection, or substitute a default value. This finite cap is a structural failure contract, not permission to run such a loop in a hard-deadline callback. A test hook must exercise exhaustion.

The address permits 2^32 events per scope and 65,536 dimensions per event. Detect exhaustion before incrementing or narrowing. Return failure to the owner; a new scope or address-schema revision requires an explicit simulation/content decision. In particular, never truncate a 64-bit world tick to the event field. If a real consumer needs larger ordinals, design and version a wider layout before it ships; do not hash the overflowing value into these fields.

Pure samples can be regenerated in any job order, chunked at different sizes, or retried on another worker. They can be rolled back by restoring the identities and event counters rather than saving a generator state for each sample. Other world updates still need deterministic commit ordering. Counter-based generators support this shape directly; see the Random123 reference and Salmon et al. paper.

The first implementation must pin an upstream reference revision, record its license/provenance, and compare against its known answers. For an all-zero key and raw all-zero counter the upstream Philox4x32-10 result is 6627e8d5 e169c58d bc57ac4c 9b00dbd8; verify the all-ones and mixed-word fixtures as well. These raw fixtures precede Ludus key derivation. Upstream vectors.

Procedural persistence

Treat reproducible procedural data as a versioned recipe: root seed, domain/address schema, generation algorithm/content revision, and stable location/object IDs. Regenerate the unmodified base from that recipe, and save gameplay mutations as explicit overrides owned by the world/content system. Changing a generation revision cannot silently reinterpret an existing save. Either retain its recipe implementation, migrate with verified identity preservation, or reject it clearly.

Nested properties need stable meanings as well as stable parent identity: adding a decorative property must not renumber a planet's layout or a room's loot. Use documented domains/dimensions and a validated child-identity scheme. Do not derive unique children with unchecked coordinate products or overlapping arithmetic seed combinations. The predictable-random-numbers chapter motivates regeneration; versioned recipes and mutation overrides are Ludus's adaptation.

Sampling contracts

Uniform sampling is a separate contract from generating bits. No standard-library distribution controls replay behavior. Every named sampler specifies its input domain, endpoints, arithmetic, number/order of raw draws, retry policy, version, and failure semantics.

Operation Contract
Raw 32-bit word All uint32 values; one PCG draw or one addressed lane
Raw 64-bit word, when required First 32-bit word is high half, second is low; two ordered draws/dimensions, with range validation before sampling
Existing NextFloat01() (raw >> 8) * 2^-24 in binary32; exactly [0,1) on the documented FP profile; one draw
Optional binary64 uniform Use the high 53 bits of the defined 64-bit word times 2^-53; [0,1); two draws
Existing stream bounded integer Existing rejection-modulo v1, bound > 0; at least one draw, variable count
New addressed bounded integer Multiply-high rejection mapping below; internal attempts isolated to the address
Chance, when required Integer Probability32 threshold in [0,2^32]; compare one raw word < threshold; even thresholds 0 and 2^32 consume one sample
Shuffle, when required Descending Fisher-Yates; for i=n-1..1, choose uniformly in [0,i+1); empty/singleton inputs consume no draws

For the new 32-bit multiply-high mapper, validate bound > 0, compute threshold = uint32(0 - bound) % bound, and draw a 32-bit r. Let product = uint64(r) * uint64(bound). Reject when low32(product) < threshold; otherwise return high32(product). Bound one returns zero but still samples once. A prepared bound may store bound/threshold when a consumer reuses it. Define it through a checked factory; never accept inconsistent caller-authored fields.

This is an unbiased mapping given uniform raw draws, with acceptance probability greater than one half. Precomputing threshold removes division from repeated draws; the uncached setup may still divide. Repeated PRNG calls are deterministic, so expected retry claims use a randomness model and provide no deadline bound. The optimization is grounded in Lemire's algorithm. If profiling justifies offering it for PCG streams, add a separately named, versioned TryNextBoundedMultiply function. It changes mapped outputs and retry consumption; never silently optimize TryNextBounded into this algorithm.

Do not calculate inclusive ranges as max-min+1 in a narrow integer. Expose half-open intervals initially; raw generation supplies the full unsigned range. If signed/inclusive or 64-bit bounded ranges become necessary, specify their overflow-safe wider arithmetic and failure contract separately. Probability authoring converts to fixed integer thresholds through a validated cooker path; record the quantization rule. Avoid floating-point percentage comparisons for authoritative choices when an integer representation suffices.

Fisher-Yates is unbiased conditional on unbiased bounded selections. Specify the supported container size before narrowing its bound. A v1 stream shuffle can use the existing unbounded-rejection sampler after validation; it is unsuitable for hard deadlines. Addressed shuffles require an explicit per-swap dimension/event layout and a failure-safe storage plan; defer that adapter until needed.

Weighted selection and authored gameplay policies

Ship the smallest adequate weighted selector first: integer weights, validated total, one unbiased ticket in [0,total), and the first cumulative weight strictly greater than the ticket. Zero weights are never selected; an empty or all-zero table, total overflow, and invalid indices fail without consuming randomness. Initially constrain the total to UINT32_MAX. Larger totals need an explicitly designed unbiased 64-bit bounded sampler, not lossy float normalization.

Use a linear scan for small tables. For larger immutable tables, store integer cumulative weights and binary-search them. Cook stable entry ordering and weights; tools preserve designer item IDs when reordering. The selected runtime index is resolved to that stable item ID before persistence. Changing weights/order changes the content identity and may change outcomes.

Only when repeated sampling repays construction and cache cost, add a cooked Vose alias table. Build in O(n) with stable worklist/tie ordering and immutable runtime storage. Do not rebuild it during each draw or use a sort-per-entry builder. A straightforward exact integer implementation stores scaled masses n * weight in checked uint64 and, per column, a cutoff in [0,total] plus an alias index. Choose a uniform column and an independent uniform ticket in [0,total); select the column if ticket < cutoff, otherwise its alias. This avoids floating threshold quantization. Validate n*weight, totals, and aliases in the cooker; addressable selection reserves distinct column/ticket dimensions. The alias table lookup is O(1), but unbiased variate generation still has rejection and no universal worst-case O(1) latency. The alias-method chapter informs this optional path.

For a one-pass candidate stream of unknown length, reservoir sampling can save storage; introduce it only for such a consumer. A sequential reservoir requires stable traversal to reproduce its outcome, and its arithmetic/weight policy is part of replay. It is not automatically order-independent because its random source is addressable. A parallel/order-independent selection scheme needs its own proved weighting, reduction, and tie contract. The proposed many-area-light resampling estimator is such a consumer: a fixed-order candidate stream with one addressed ticket dimension per candidate. FoundationMath ships the sequential weighted reservoir for it.

Raw randomness never quietly implements fairness. Shuffle bags, no-repeat sound choices, weighted-without-replacement selection, streak protection, cooldowns, and pity counters are separate game policies. Their persistent state includes bag order/cursor, previous item, counters, and policy/content version. Record when each policy commits state, and how save/load, death, restart, and rollback affect it. Any adjusted distribution must be visible in authoring/debugging.

AI selects random parameters or decisions at defined decision events, then keeps them until its authored reevaluation/cooldown rule fires. Sampling every frame can make agents flicker and creates frame-rate dependence. Different event domains isolate behavior tuning from combat/loot decisions. This persistence rule is a Ludus policy decision.

AI planning may need the outcome probabilities themselves, not just a sampled result. Expose the immutable authored integer weights/model to the planner without consuming simulation RNG. The planner and actual outcome sampler must use the same content/policy revision; include current pity/cooldown state when it changes the outcome model. Rollouts have their own domain and deterministic scenario IDs, and cannot advance live gameplay streams or peek at their future draws. Planner tie breaking and iteration budgets are explicit simulation policies. The NPC decision chapter motivates this separation of stochastic modeling from actual outcome generation; a general dynamic-programming engine remains outside FoundationMath.

Sequential weighted reservoir

weighted_reservoir.hpp and its kernel port shaders/weighted_reservoir.slang select one candidate from a stream, with replacement, for resampled importance sampling (RIS). The reservoir is the streaming selection of Chris Wyman's Ray Tracing Gems II chapter 22, specialized to one sample. Selection version 1 (WEIGHTED_RESERVOIR_SELECTION_VERSION) freezes this contract:

  • Order. The caller streams every candidate, zero weights included, in one fixed order. Each candidate supplies a weight and its own ticket in [0,1), normally SampleFloat01 at a dimension reserved for that candidate. The outcome depends on that order; an addressable random source does not make it order-independent.
  • Rule. w_sum += w, then the candidate replaces the selection when ticket * w_sum < w. The comparison is strict, so a zero weight is never selected and the first positive weight always is. The reservoir is empty exactly while w_sum = 0. A zero total is a valid empty result that contributes zero, not an error.
  • Count. The reservoir counts every streamed candidate as RIS M and records the selected candidate's stream position. It stores no payload: the caller keeps the payload of the candidate whose update reports a selection.
  • Contribution weight. TryComputeReservoirContributionWeight returns W = w_sum / (M * p̂(y)) for the selected candidate's target p̂(y). An empty reservoir returns W = 0 without examining the target.
  • Validation. Weights and tickets are classified on their FP32 bits, never through NaN comparisons. A NaN or infinite weight, ticket or target returns NonFiniteInput. A negative weight, a ticket outside [0,1), or a zero, negative or subnormal target returns InvalidArgument. A weight sum, M * p̂(y) or W that would overflow returns OutOfRange. Every failure leaves the reservoir and outputs unchanged. -0 is a zero weight and a valid zero ticket.
  • Declared approximations. Weights below WEIGHTED_RESERVOIR_MINIMUM_WEIGHT (2^-102, 2^24 times FP32's smallest normal value) count as zero, and so do subnormal tickets. Above that threshold ticket * w_sum is normal wherever it can approach w, so neither FP32 rounding nor GPU subnormal flushing can change a selection. A subnormal W is flushed to zero. Tickets quantize acceptance to their resolution, 2^-24 for SampleFloat01.

The kernel port returns false instead of a status and leaves the reservoir unchanged on failure. Metal compiles shaders with fast math, so the port tests bits rather than relying on IEEE infinities or NaNs. Selections and weight sums use only correctly rounded FP32 additions and multiplications, so they match the CPU bit for bit. W divides once, and a backend's FP32 division may differ from the CPU's correctly rounded quotient within that backend's division accuracy; WGSL, for example, allows 2.5 ULP.

FoundationMath's unit tests enumerate every ticket tuple on a midpoint grid whose size is a multiple of every prefix sum. Selection counts then equal the weight ratios exactly, for example 216:432:0:648 for weights 1:2:0:3 over 1296 streams. The tests also cover zero totals, a single candidate at the largest ticket, the strict boundary, rejection and output preservation, and E[W * g(Y)] = Σ g over an enumerated two-candidate RIS estimator. The shared GPU corpus in GPU addressed sampling replays these cases, addressed RIS streams and contribution-weight edges on every backend, choosing power-of-two denominators M * p̂(y) so that every backend divides exactly. The restir-di render-graph template keeps its private reservoir until it adopts this helper in a separate change.

Spread, nonuniform distributions, and graphics

Geometry and distribution names state the measure being sampled. Uniform disk area uses radius = R * sqrt(u) and angle = 2*pi*v; a uniform radius clusters probability near the center per unit area. Uniform sphere directions sample uniform z in [-1,1] and uniform azimuth, not uniform latitude. A uniform cone uses uniform cosine within its angle bounds. Validate finite parameters and reserve the documented uniform dimensions. Transcendental geometry follows the existing Math numerical contract, rather than promising cross-platform bits.

If a game needs Gaussian jitter, begin with a named normal-pair sampler using Box-Muller, fixed two uniforms, and no hidden cached spare. With u,v in [0,1), use r = sqrt(-2*log(1-u)), then r*cos(2*pi*v) and r*sin(2*pi*v). Define intermediate precision, reject invalid parameters, and check finite results before commit. Finite random-bit resolution truncates reachable tails, and libm results are numerically qualified rather than bit-exact across platforms. An integer approximation/LUT for authoritative cross-platform spread would be a separate measured, versioned feature.

Two independent centered normals of equal variance are isotropic: their joint density is proportional to exp(-(x*x+y*y)/(2*sigma*sigma)). It depends only on radius. The contrary axis-bias claim in Game Programming Gems 7 is rejected. The chapter's absolute-normal radius plus uniform angle is a distinct radial profile, not a 2D Gaussian. Three summed uniform samples are a bounded bell-like approximation, not an exact normal. If useful, name that policy Bell3 and expose its support/variance explicitly. Truncation, clamping, disk spread, cone spread, and normal spread are different authored choices. See the review correction and PBRT distribution treatment.

Renderer quality has a different objective: reducing error and organizing samples over dimensions, pixels, and frames. A high-quality IID generator does not replace stratified/Sobol sampling or blue-noise masks. Keep those in renderer sampling, with explicit sample dimensions and reset/history policy; see the PBRT sampling interface. Cosmetic particles, audio variation, and rendering have separate domains and owners. Changing render rate, visibility, debug draws, or audio completion timing must never advance authoritative simulation streams.

GPU addressed sampling

Kernels generate Philox samples in place instead of uploading a buffer of random numbers. The shared primitive is shader source owned by FoundationMath, modules/foundation/math/shaders/math.slang, installed beside the other SDK shader includes; general GPU math/random primitives belong there rather than in each consumer. That ownership adds no C++ dependency: the FoundationMath library never depends on RHI or shader tooling. Consumers own everything else: the renderer chooses its key, scopes, dimension schema and failure handling, and writes its sampling code in its own kernels.

The port implements addressed random layout v1 and bounded mapping v1 exactly:

Shader CPU contract
RandomAddress { uint2 Scope; uint Event; uint Dimension; } RandomAddress; Scope is (low32, high32) of the 64-bit scope because WGSL has no uint64
Philox4x32(counter, key) Philox4x32-10; key.x/key.y are the low/high words of RandomKey::Value
SampleRandomUInt32, SampleRandomFloat01 SampleUInt32, SampleFloat01
TrySampleRandomBounded(key, address, bound, out value) TrySampleBounded; returns false for bound zero, an out-of-range dimension, or after 65,536 rejected attempts
RANDOM_MAX_DIMENSION, RANDOM_ATTEMPT_COUNT, version constants RandomMaxDimension, RandomBoundedAttemptCount, RandomAddressLayoutVersion, RandomBoundedMappingVersion

Shader integers are 32 bits, so the address limits are explicit caller contracts. A dimension must not exceed 65,535: check it with IsRandomAddressInRange or prove it from admitted limits. The packing masks the dimension to 16 bits, so a violation aliases dimension % 65536, exactly as CPU uint16 narrowing would; it can never spill into the attempt bits and alias another address's retry. The bounded sampler rejects out-of-range dimensions. Bounded failure is a bool result, not a sentinel output value, and callers must branch on it. The GPU preview path tracer, for example, fails the sample. A 32-bit owner identity uses uint2(identity, 0), as the CPU reference path does with its pixel scopes.

WGSL's ordinary integer vocabulary cannot reuse C++'s uint64 multiplication, so MultiplyWide forms the 64-bit product from 16-bit limbs. The WGSL specification defines these integer types. Each partial sum stays below 2^32. The compute kernel tests/sdk_consumer/random_parity.slang evaluates the cases built by tests/sdk_consumer/random_parity_corpus.h, which every backend must reproduce exactly:

  • the three pinned Random123 known answers and the Ludus v1 golden answers, including the forced-rejection address;
  • live parity with SampleUInt32, SampleFloat01 and TrySampleBounded across both key and scope words, event extremes, lanes 0-3/4-7, the top of the dimension range, out-of-range dimensions and rejection-heavy, 16-bit-crossing and zero bounds;
  • the production retry loop with injected draws: exhaustion with a source that would accept one extra attempt, acceptance on the last attempt, and a unit bound;
  • endpoint float mappings and the shader's copies of the versions and limits;
  • MultiplyWide over every pair of limb-edge values and exhaustive sweeps of each limb against an all-ones operand;
  • the sequential weighted reservoir: its shader constants, every stream of the 1:2:0:3 exact-frequency ticket grid, 256 addressed eight-candidate RIS streams, and zero-total, strict-boundary, minimum-weight, subnormal-ticket, rejection and contribution-weight edges. A reservoir case is followed by its candidate cases, which its invocation reads in order.

The renderer's GPU test modules/graphics/renderer/tests/gpu_random_tests.cpp runs the corpus on Metal (macOS) and Vulkan (Linux GPU CI) and skips only when no Metal adapter exists. The installed-SDK fixture tests/sdk_consumer/random_parity.cpp runs the same corpus through WebGPU in the Browser backends and packages workflow: tests/sdk_consumer/random-parity-browser-test.mjs loads it in the pinned Chromium and requires every result word to match the CPU reference compiled into the same WebAssembly module. CI uses Chromium's SwiftShader adapter, so this establishes the WGSL translation and WebGPU compute path; hardware WebGPU adapters are not yet qualified. Benchmark the limb cost and register pressure before optimizing. Keep CPU parity required only for consumers that actually need it; any alternate cosmetic hash is a separately versioned visual algorithm, never an unnoticed replacement for Philox simulation samples.

Replay, persistence, networking, and entropy

World replay/checkpoints store root seed, build/content identity, an RNG contract manifest ID, and the relevant states. The manifest identifies algorithm variants, seeding/derivation version, address layout, each sampler mapping version, and game policy schema. Record stable domain/dimension manifests with the content. RandomState.Version alone cannot establish distribution or gameplay compatibility.

Serialize PCG v1 as explicit little-endian fields: version uint32, state uint64, increment uint64 (20 bytes of payload), accompanied by owner identity and enclosing format/length metadata. Do not dump the in-memory structure; its padding/layout is not the wire format. Validate supported version, odd increment, owner uniqueness, lengths, and manifest compatibility before atomically restoring the world. Addressed sampling stores root/domain identity and owner event/identity counters, with no sample cache. Gameplay policy state is serialized separately.

Seed-only replay works only when initial state, logical input sequence, sampler versions, content, event assignment, and sequential draw order are identical. Mid-session checkpoints need actual stream/policy states and event counters. Include them in canonical simulation hashes in stable owner order. Existing game-world replay is primarily same-build debugging; this proposal does not silently upgrade it to network lockstep.

For hot reload, preserve states if the RNG contract is compatible; if code/content changes semantics, require an explicit restart or compatible migration. Changing a root seed mid-session is a deliberate restart/reseed command, with world-owned policy for all streams/counters. Save files and replays reject unsupported contracts clearly. Retain old mapper implementations only for supported historical formats; do not emulate missing behavior by reseeding the newest algorithm.

Multiplayer authoritative decisions belong to the authority. Replicate the required outcomes or agreed simulation state; deterministic client prediction uses identical contracts and rollback state. A seed disclosed to clients provides reproducibility, not outcome secrecy or anti-cheat. Decisions whose predictability matters use a separately designed trusted-side security mechanism.

Platform's optional TryFillSecureRandomBytes returns an explicit failure and leaves caller output unchanged, staging a bounded request if the backend can partially fill it. Use maintained platform APIs, not an engine-designed entropy mixer. Root-seed setup must return failure to the caller when entropy is unavailable; deterministic mode explicitly supplies a seed. No silent fallback to time, addresses, user input timing, or uninitialized memory. Security clients request secure bytes directly and never use simulation PCG/Philox or Mix64. The entropy chapter review records why its historical collection strategy is not adopted.

Debugging and observation

Make a failure reproducible from its logical context. A development tool reports root seed, domain name/ID, scope/event/dimension, algorithm/mapper contract, relevant stream snapshot, requested distribution parameters, and content revision. For sequential streams, report a world-owned raw-draw count when tracing is enabled; it counts rejected draws too. A counter per domain is insufficient when there are multiple owners.

Optional instrumentation lives in runtime/tool adapters. Use preallocated bounded records with logical tick and owner IDs; merge records by logical identity rather than wall-clock arrival order. A full buffer drops diagnostic records and reports a dropped count without changing RNG results. Do not add a logger/profiler pointer, branch, allocation, or lock to the production generator state. Raw-draw counts are diagnostic state, not new mandatory bytes in RandomStream.

Compare two replays at the first divergent canonical world hash, then inspect event assignment, draw counts, and sampler inputs. Addressed decisions can be evaluated directly in a standalone fixture. A bounded sampler must allow tests to supply deterministic raw words, force rejection/exhaustion, and compare algorithms without relying on rare PRNG sequences. Keep that adapter private or test-only. Visual histograms and spread plots explain distributions; they do not prove correctness or cryptographic quality.

Validation and performance gates

Gate Required evidence
Compatibility Existing PCG fixtures unchanged; pinned upstream Philox zero/all-ones/mixed vectors; derivation/address/sampler golden corpus
Arithmetic and bounds Zero/max bounds, threshold edges, forced rejection, endpoint floats, full probability range, counter/dimension overflow, signed conversion audit
Unbiased mapping Exhaustive reduced-width model over every bound/raw value; forced rejection fixtures against an independent reference; no modulo-only shortcut
Ownership Same inputs with reversed/shuffled entity/job order, different worker counts/chunk sizes; identical addressed outputs by logical identity
Isolation Add cosmetic draws, disable rendering/audio, add a dimension, and change unrelated domain content; existing authoritative choices remain unchanged
Save/rollback/reload Mid-sequence restore; policy/event/ID allocator restoration; duplicate owners and corrupt/unsupported manifests rejected without partial restore
Weighted/policy Exact small integer tickets and alias mass checks; zero/overflow weights; deterministic bags and content changes; policy statistical expectations tested separately
Portability Identical raw/mapping corpus on pinned native toolchain and pinned Emscripten; GPU known-answer, raw/float/bounded parity, retry-loop and limb-product corpus on Metal, Vulkan and WebGPU (Chromium SwiftShader against the WebAssembly CPU reference; GPU addressed sampling); hardware WebGPU adapter and ARM qualification before claiming their parity
Integration Warning-clean builds, unit and ASan/UBSan tests, format/tidy, header/include/build-budget gates, installed SDK consumer
Quality Offline PractRand/TestU01 and targeted interleaved-domain/nearby-scope patterns, high/low bits, lane patterns; record tool versions, corpora, failures and data volume

Statistical tests use predetermined seeds/criteria and adequate volume in an offline qualification job. A short noisy histogram is not a per-commit gate and no finite battery proves independence. Re-run qualification when the algorithm, round count, constants, packing, derivation, or tested cross-stream pattern changes. Correctness fixtures remain fast deterministic CI tests.

Benchmark shipping optimization levels on supported hardware/toolchains. Consume outputs so the compiler cannot eliminate the work; report generator, seeding, bounded-variable/constant/prepared bounds, a real loot/AI workload, and batch Philox with all four lanes used. Include adversarial rejection bounds, cold/warm tables, construction time, code/state size, allocations, and p50/p95/p99 sampling cost where meaningful. End-to-end frame/chunk workload savings decide whether an optimization ships. Do not infer engine speed from a published RNG loop.

Optimize in this order: avoid unintended draws; derive keys once; use the block API for four needed dimensions; prepare reused bounds; measure LTO/out-of-line cost; only then add SIMD behind the same exact contract. No fast math or weakened round count. New batched bounded-generation research is a useful later benchmark candidate, but its different consumption and complexity do not justify changing the compatibility path. See Brackett-Rozinsky and Lemire.

Delivery plan and alternatives

Implement the initial architecture before evaluating the later research below. The PCG32 v1 and addressed Philox primitives are the starting point; their reference, boundary, rejection-budget, allocation and worker-order fixtures, installed-SDK checks and representative WebAssembly corpus are already in the repository. So are the shader port in FoundationMath's math.slang, its renderer GPU-preview consumer and its Metal/Vulkan parity test. A real game/world consumer and its persistence protocol remain to be delivered.

Remaining initial implementation

Work Owner Required result
Stable integration contract and one addressed consumer Game/world and content Inventory existing consumers; assign persistent domain/dimension meanings and scope identities; define event commit/exhaustion rules; integrate a procedural or parallel gameplay consumer with scheduling and presentation isolation regressions.
Named sequential setup helper FoundationMath Implement the domain-key/scope PCG seeding convention from stable keys with golden answers; preserve existing explicit seed schemes and PCG v1 behavior.
State codec and replay compatibility Serialization and game/world Encode the explicit 20-byte little-endian PCG payload; validate owner identity, lengths and RNG/content manifests before atomic restore; checkpoint streams, policy state, event counters and identity allocators; include them in canonical hashes and test rollback/reload incompatibility.
Procedural persistence Game/world and content Persist generation recipes/revisions and stable nested identities; save mutations as overrides; retain, migrate or clearly reject unsupported recipes.
Required samplers FoundationMath and content cooker Begin with small integer cumulative weighted selection; add probability, shuffle or spread functions only when the selected consumer needs them, with versioned consumption, checked arithmetic and failure-preservation tests.
Authored policies and planning models Game/domain and tools Persist the required bag/no-repeat/pity/cooldown or decision state; let planners read the same immutable outcome model without advancing live RNG; isolate rollout domains.
Optional automatic seed acquisition Platform and application setup Add checked OS/browser secure bytes when automatic seeds are required; acquire and record one root seed; report failure without an implicit fallback. Explicit deterministic seeds remain valid.
Observation and qualification Runtime/tools and validation tooling Add bounded logical-context tracing and per-owner raw-draw counts where needed; complete consumer save/isolation regressions, native/web acceptance and offline statistical qualification; record representative performance baselines before optimization.
Hardware WebGPU qualification Graphics validation Software-adapter WebGPU parity runs in the browser job. Run tests/sdk_consumer/random-parity-browser-test.mjs on representative hardware WebGPU adapters and record the results before claiming parity on them.

Deliver this work in the following order:

  1. Freeze the integration contract and integrate one addressed consumer. Define stable domains, identities and replay compatibility metadata with its owner. Exercise job reordering, different worker/chunk counts, cosmetic draws, rendering/audio changes, counter exhaustion and output preservation.
  2. Complete setup and persistence. Add the sequential setup convention and explicit state codec, then verify checkpoints, procedural recipes, atomic restore and compatible reload for that consumer.
  3. Add demonstrated sampling and policy needs. Start with the smallest adequate integer weighted selector, then implement the consumer's required probability/shuffle/profile and saved policy state. Add seed acquisition and observation adapters when that workflow requires them.
  4. Qualify the initial implementation. Apply the validation gates to the delivered scope, including offline statistical tests and a reproducible workload baseline. Include Doxygen contracts, canonical usage documentation and native/web evidence in each feature change. Record deliberate deferrals explicitly; unused optional samplers are not prerequisites for the first consumer.

Milestone 1: procedural placement

The first addressed consumer outside the renderer is room-scoped scatter of static placements in apps/world_demo. It is also the P0 slice of the procedural content generation architecture, which owns the recipe, placement identity, thinning rule and kernel. This section owns the randomness contracts that consumer exercises: domains, root seed, event rules and isolation. Weighted selection, the state codec and seed acquisition stay in later milestones; the scatter chooses among equally likely variants with TrySampleBounded.

Root seed. The game session takes an explicit uint64 world seed, with a fixed default, and records it with the session. Every room derives its scatter keys from that seed and a manifest domain. The room's identity enters through the address scope, so two rooms never repeat each other's scatter. Each level keeps its authored Seed as content data; the scatter does not use it, because levels that shared a value would generate identical placements.

Domains. The game manifest assigns one domain per property, so a new draw for one property cannot shift another: placement (position and thinning priority), variant, transform, regrow and cosmetic. Base generation uses event zero in the placement, variant and transform domains; the procedural document lists their dimensions. Rendering samples only the cosmetic domain.

Event rules. Harvesting a placement records a tombstone for its identity. A tombstone is a world mutation, not a random event, and survives leaving and re-entering the room, because regeneration reapplies it. Regrowing a harvested placement draws its new variant at (regrow domain, placement scope, ordinal, 0):

  • The per-placement regrow ordinal starts at one and advances exactly once, in the commit phase of the tick that commits the regrow.
  • Rebuilding a candidate world, retrying a step or faulting a tick neither advances nor publishes an ordinal. A retried evaluation reuses the same ordinal.
  • An ordinal that would pass UINT32_MAX fails the regrow with an explicit status, and the placement stays harvested. It never wraps or narrows.
  • Tombstones and ordinals enter the world replay hash in milestone 1 and the checkpoint in milestone 2. Cosmetic samples enter neither.

Isolation regressions. The milestone is accepted when tests show that:

Change Must not change
Worker count, chunk size, traversal order, or building one cell alone instead of with its neighbours Accepted placements, positions, variants and transforms
Adding a domain or dimension, changing the transform ranges Candidate positions, priorities and acceptance
Disabling rendering, changing frame pacing or interpolation, extra cosmetic draws The world replay hash
Editing another room's recipe, adding a layer Existing rooms and layers
Rebuilding, retrying or faulting a tick Regrow ordinals and tombstones

The same fixed seed and recipe produce a checked-in placement digest on native and Emscripten builds. Scope overflow, duplicate IDs and capacity overruns are rejected before generation, with nothing published.

Delivery. Three pull requests deliver it, each with its tests and documentation:

  1. Delivered: the domain manifest check in FoundationMath and the headless scatter kernel in Ludus::ProceduralEnvironment, with the golden, ordering, property-isolation and rejection tests.
  2. Delivered: world demo integration. The checked-in manifest (apps/world_demo/internal/random_domains.h) assigns the five domains, the room catalog (internal/room_catalog.h) maps each level's document identity to its ordinal, the .world.json rooms author scatter, scatter_layer and scatter_variant entities, the session records an explicit WorldSeed, the room's placements are generated when its world is prepared and enter the replay hash, and extraction draws them with a cosmetic-domain tint that enters nothing. The world demo guide describes authoring and limits; apps/world_demo/tests/scatter_tests.cpp and the browser harness check the isolation rows above, catalog/manifest rejection, load failures that publish nothing and one pinned placement digest.
  3. Delivered: harvesting and regrowth with the event rules above. One scatter_layer may be marked harvestable in the level data (at most one per room); overlapping a placement records a tombstone keyed by its packed scatter scope, which regeneration reapplies, so it survives leaving and re-entering the room. A harvested placement regrows by drawing a new variant with TrySampleBounded at (RANDOM_DOMAIN_SCATTER_REGROW, placement scope, ordinal, 0); the per-placement ordinal starts at 1 and advances exactly once in the Commit phase of the committing tick. Tombstones and ordinals enter the replay hash; cosmetic samples do not. The storage is bounded and allocated when the room is prepared, never on the tick path. apps/world_demo/tests/harvest_tests.cpp checks that the ordinal advances exactly once per committed regrow, that the BuildNext and fault paths leave it alone, that forced exhaustion fails and preserves the outputs, that tombstones survive a room round-trip, and that the replay hash tracks harvest and regrow but not rendering or cosmetic draws.

Research after the initial implementation

Use the initial implementation and its recorded workload/quality baseline to evaluate improvements. This is a reading backlog: bibliographic details and abstracts were checked, but a detailed algorithm review and Ludus experiments remain future work. It supplements the completed Gems review without changing the existing generator or replay contracts.

Venue Recommended source Question for Ludus
Winter Simulation Conference (WSC) Pierre L'Ecuyer, Olivier Nadeau-Chamard, Yi-Fan Chen and Justin Lebar, Multiple Streams with Recurrence-Based, Counter-Based, and Splittable Random Number Generators, WSC 2021, especially sections 3, 5 and 6 Do our key/seed conventions and interleaved domain/scope/lane qualification patterns cover the risks of multiple streams and counter-based sampling?
ACM Transactions on Mathematical Software (TOMS) Pierre L'Ecuyer and Richard Simard, TestU01: A C Library for Empirical Testing of Random Number Generators, 33(4), article 22, 2007 How should we expand qualification of actual Ludus output patterns and report data volume, individual test results and reproducible failures?
ACM TOMS Lorenz Hübschle-Schneider and Peter Sanders, Parallel Weighted Random Sampling, 48(3), article 29, 2022; author preprint When do cooked alias construction, replacement semantics or parallel reservoir algorithms improve a demonstrated weighted workload?
ACM Transactions on Modeling and Computer Simulation (TOMACS) Daniel Lemire, Fast Random Integer Generation in an Interval, 29(1), article 3, 2019 The addressed mapper already adopts this work. Use it as a correctness and benchmark reference when extending bounded mapping and shuffling.
Software: Practice and Experience Nevin Brackett-Rozinsky and Daniel Lemire, Batched Ranged Random Integer Generation, 55(1), 2025; author preprint Can a separately versioned batch/shuffle API reduce draw and mapping cost enough to justify its different consumption contract?
SC (high-performance computing conference) John K. Salmon, Mark A. Moraes, Ron O. Dror and David E. Shaw, Parallel Random Numbers: As Easy as 1, 2, 3, SC11, 2011 The current Philox design already adopts this work. Revisit its parallel generation and state-cost analysis for a real SIMD/GPU consumer.
ACM-SIAM Symposium on Discrete Algorithms (SODA) Thomas L. Draper and Feras A. Saad, Efficient Online Random Sampling via Randomness Recycling, SODA 2026, pp. 2473-2511; author paper Does randomness recycling benefit our discrete-sampling workload after accounting for its retained state, checkpoint requirements and consumption changes?

Start the detailed review with the WSC survey, TestU01 and parallel weighted sampling. Treat batching, randomness recycling, cooked alias tables, reservoir selection, SIMD blocks, faster GPU products and alternate generators as subsequent experiments driven by a real consumer.

For each experiment, pin the source revision, preserve attribution/licensing, state the adopted idea and its departures, and compare correctness, allocations, state/code size and end-to-end cost with the recorded baseline. Add any new cross-domain or nearby-scope statistical tests suggested by the review. Preserve PCG v1 and the existing addressed v1 contracts; consumption/state/mapping changes need a named, versioned API and compatibility review. Published speedups and test results are evidence about the authors' implementations, not Ludus results.

Alternatives

Alternative Assessment
PCG32 only Smallest current implementation; sufficient for ordered v1 gameplay. Lacks direct addressed parallel sampling without another construction.
Philox everywhere Coherent pure-function option for a new design. Existing stream compatibility and straightforward sequential APIs justify retaining PCG in Ludus.
xoshiro256++ Credible compact sequential benchmark candidate; adds a second stream/state/migration contract without a demonstrated Ludus gain. Author reference.
MT19937/WELL Historical strengths do not justify larger per-owner state for these game workloads. Retain neither as a default based on period alone.
Small GPU hash everywhere May suit separately qualified cosmetics; does not supply the selected simulation generator/replay contract.
OS CSPRNG everywhere Correct source for security and root entropy. Simulation still needs explicit deterministic ownership and stable replay sampling.
Global or automatic thread-local stream Hidden coupling and schedule-dependent simulation results; excluded.

Core primitives, representative native/WebAssembly fixtures, Metal, Vulkan and software-adapter WebGPU parity, and the world demo's room scatter with harvesting and regrowth are implemented (milestone 1 is delivered). The checkpoint state codec and seed acquisition, broader acceptance evidence, performance baselines, hardware WebGPU qualification and statistical qualification remain work for the later milestones above. Complete the initial implementation before using the research backlog to propose measured improvements.

Metropolis sampling and weighted radiance

metropolis.hpp owns the versioned symmetric toroidal primary-sample mutation, positive-target MH acceptance, Jump Restore competing-event rates/exponential times, the practical RB tour recurrence and transactional compensated FP32 weighted radiance sums. shaders/metropolis.slang implements the same arithmetic on the GPU. Each caller owns one tour and accumulator per chain; there is no allocation, shared mutable RNG or hidden reset. Invalid input and overflow preserve CPU outputs. The shader reports failure before publishing its candidate state. Zero-weight updates are no-ops and an empty sum resolves to black.

The MH template contract owns transport, random scopes, iteration counts, lifecycle and the estimator's finite-sample bias. It credits Holl, Singh and Seidel (2026), section 5.4 and Algorithms 5.2/6.2 and section 6. Jump Restore uses unit local rate and a uniform restoration kernel with killing rate restorationScale/target. Independent uniform tickets select the event and exponential holding time; RB replaces that time with its conditional mean. Rates are computed without adding large targets or forming an overflowing killing rate. These primitives do not make arbitrary MCMC chains stationary or their self-normalized estimates unbiased; the caller must establish proposal symmetry, positive support and correct numerator/denominator measures.

Stochastic pairwise MIS

FoundationMath owns the scalar CPU/shader primitives documented in the Math guide. Thanks to Trevor Hedstrom, Markus Kettunen, Daqi Lin, Chris Wyman and Tzu-Mao Li, Stochastic Pairwise MIS for Unbiased Large-Kernel Reuse in Real-Time, Computer Graphics Forum 45(2), 2026, equations 10–19 and section 4.3. This is an independent implementation of their defensive pairwise arithmetic and with-replacement correction.

At a destination sample, evaluate every source proxy in the destination measure: sourceTarget / abs(forwardJacobian), with zero proxy for a failed shift or impossible inverse proposal. The source UCW remains in its original measure. Multiply the resampling weight by the forward Jacobian separately. Using the raw source target in MIS while only multiplying the resampling weight by the Jacobian is incorrect for non-unit shifts.

Scale all neighbor confidences and their sum by the same defensive factor. Accumulate the canonical base plus independently sampled canonical correction terms. Correct every drawn term by its own actual drawCount * selectionPmf; repeated selections retain multiplicity. A proposal must retain positive support where its MIS-weighted contribution can be nonzero. Reserve separate addressed random dimensions for initial samples, canonical draws, neighbor draws and the reservoir selection coins. Final UCW is weightSum / selectedTarget; applying the ordinary RIS M division again changes the estimator.

The library cannot establish unbiasedness alone: the consumer owns valid input UCWs, consistent targets, shift Jacobians, forward/inverse support and visibility. Renderer owns the storage-independent clipped screen-tile CDF sampler and the DI/GI adapters; their workflow and limitations belong to the render-graph guide. Independent finite-domain enumeration and CPU/GPU/installed-SDK parity cover the scalar operations, zero support, non-unit Jacobians and invalid inputs. They do not establish the full path-space algorithm or a device performance gain.