Texture streaming - #34
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds configurable texture streaming with resident-mip tracking, asynchronous D3D12 uploads, and memory statistics logging. It also adds deterministic Sobol/Owen sampling utilities and routes pre-generated samples into ray-traced BSDF sampling. ChangesTexture Streaming Pipeline
Deterministic Ray-Tracing Sampling
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant TextureHook
participant TextureManager
participant D3D12Renderer
participant TextureStatsAPI
TextureHook->>TextureManager: Register resident mip offset
TextureHook->>D3D12Renderer: Submit resident-mip upload with event query
D3D12Renderer-->>TextureHook: Report upload completion
TextureHook->>TextureManager: Create descriptor with resident metadata
TextureStatsAPI->>TextureManager: LogMemoryStats()
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Core/TextureManager.cpp`:
- Around line 97-103: The standardBytes counter is accumulating all textures
without excluding streamed ones, so streamed textures are being counted in both
standard and streamed categories. Modify the code to only add to standardBytes
when the texture is not a streamed texture by adding a condition that excludes
textures where residentMipOffset is greater than 0. This will ensure the
standard and streamed byte counts are mutually exclusive and the log breakdown
is accurate. Apply the same fix to the similar code block referenced at lines
112-119.
- Around line 24-31: The RegisterResidentMipOffset function in
TextureManager.cpp stores raw IUnknown pointers as keys in the
g_ResidentMipOffsets map, but these entries are only removed when GetDescriptor
consumes them. If a registered resource is destroyed without reaching descriptor
creation, the stale pointer key persists in the map. When another COM object
later reuses the same memory address, the old entry will incorrectly apply a
stale mip offset to the new object. Implement proper cleanup by registering a
release callback or using COM object lifecycle tracking (such as implementing
IUnknown::Release wrapping or using weak_ptr alternatives) to remove entries
from g_ResidentMipOffsets when resources are actually destroyed, rather than
deferring cleanup until descriptor consumption.
In `@src/Hooks.cpp`:
- Around line 113-155: The GetTextureStreamingMipBias function determines
texture streaming bias but never uses the TextureBudgetMB setting from
ExperimentalSettings, making the memory budget configuration ineffective.
Integrate TextureBudgetMB into the bias calculation by computing an estimated
memory footprint for the texture at different mip levels, then adjust the
desiredBias upward as needed to keep memory consumption within the configured
budget limit. This adjustment should happen after the initial bias is determined
by TextureStreamingMode but before the final clamping operations, ensuring the
budget constraint overrides mode-based decisions when memory limits would be
exceeded.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a30ea6a0-e526-4e30-869d-897c421e9496
📒 Files selected for processing (10)
AGENTS.mdextern/RTXDI-Librarysrc/API.cppsrc/API.hsrc/Core/MaterialBase.cppsrc/Core/Texture.hsrc/Core/TextureManager.cppsrc/Core/TextureManager.hsrc/Hooks.cppsrc/Types/Settings.h
💤 Files with no reviewable changes (1)
- AGENTS.md
| void TextureManager::RegisterResidentMipOffset(IUnknown* resource, uint32_t mipOffset) | ||
| { | ||
| if (!resource || mipOffset == 0) | ||
| return; | ||
|
|
||
| std::scoped_lock lock(g_ResidentMipOffsetsMutex); | ||
| g_ResidentMipOffsets[resource] = mipOffset; | ||
| } |
There was a problem hiding this comment.
Resident mip-offset registry can retain stale pointer entries.
Offsets are only removed on GetDescriptor consumption. If a registered resource never reaches descriptor creation, the raw-pointer key persists and can later be reused by another COM object, applying an incorrect mip offset.
Suggested mitigation
void TextureManager::ReleaseTexture(RE::BSGraphics::Texture* texture)
{
if (!texture)
return;
IUnknown* key = nullptr;
@@
`#endif`
key = texture->texture;
+ {
+ std::scoped_lock lock(g_ResidentMipOffsetsMutex);
+ g_ResidentMipOffsets.erase(key);
+ }
+
m_Textures.erase(key);
m_NormalMaps.erase(key);
}Also applies to: 172-179
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Core/TextureManager.cpp` around lines 24 - 31, The
RegisterResidentMipOffset function in TextureManager.cpp stores raw IUnknown
pointers as keys in the g_ResidentMipOffsets map, but these entries are only
removed when GetDescriptor consumes them. If a registered resource is destroyed
without reaching descriptor creation, the stale pointer key persists in the map.
When another COM object later reuses the same memory address, the old entry will
incorrectly apply a stale mip offset to the new object. Implement proper cleanup
by registering a release callback or using COM object lifecycle tracking (such
as implementing IUnknown::Release wrapping or using weak_ptr alternatives) to
remove entries from g_ResidentMipOffsets when resources are actually destroyed,
rather than deferring cleanup until descriptor consumption.
| standardBytes += texture->size; | ||
|
|
||
| if (texture->residentMipOffset > 0) { | ||
| streamedTextures++; | ||
| streamedBytes += texture->size; | ||
| } | ||
| } |
There was a problem hiding this comment.
standard memory in the log currently includes streamed textures.
This makes the breakdown non-exclusive and easy to misread. Either rename the label or log non-streamed bytes explicitly.
Suggested fix
- uint64_t standardBytes = 0;
+ uint64_t standardBytes = 0;
+ uint64_t nonStreamedBytes = 0;
@@
standardBytes += texture->size;
if (texture->residentMipOffset > 0) {
streamedTextures++;
streamedBytes += texture->size;
+ } else {
+ nonStreamedBytes += texture->size;
}
@@
- "TextureManager - RT texture memory: total={} MiB, standard={} MiB ({} textures), normal={} MiB ({} textures), streamed={} MiB ({} textures)",
+ "TextureManager - RT texture memory: total={} MiB, standard={} MiB ({} textures), normal={} MiB ({} textures), streamed={} MiB ({} textures)",
(standardBytes + normalBytes) / (1024 * 1024),
- standardBytes / (1024 * 1024),
+ nonStreamedBytes / (1024 * 1024),Also applies to: 112-119
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Core/TextureManager.cpp` around lines 97 - 103, The standardBytes counter
is accumulating all textures without excluding streamed ones, so streamed
textures are being counted in both standard and streamed categories. Modify the
code to only add to standardBytes when the texture is not a streamed texture by
adding a condition that excludes textures where residentMipOffset is greater
than 0. This will ensure the standard and streamed byte counts are mutually
exclusive and the log breakdown is accurate. Apply the same fix to the similar
code block referenced at lines 112-119.
| uint32_t GetTextureStreamingMipBias(const ExperimentalSettings& settings, uint32_t width, uint32_t height, uint32_t mipLevels, DXGI_FORMAT format) | ||
| { | ||
| if (settings.TextureStreamingMode == TextureStreamingMode::Off || mipLevels <= 1) | ||
| return 0; | ||
|
|
||
| const uint32_t maxDimension = std::max(width, height); | ||
| if (maxDimension < 1024) | ||
| return 0; | ||
|
|
||
| if (IsCriticalStreamingFormat(format)) | ||
| return 0; | ||
|
|
||
| uint32_t desiredBias = 0; | ||
| const bool alphaCapable = IsAlphaCapableFormat(format); | ||
| switch (settings.TextureStreamingMode) { | ||
| case TextureStreamingMode::Conservative: | ||
| if (alphaCapable) | ||
| return 0; | ||
|
|
||
| desiredBias = maxDimension >= 4096 ? 1 : 0; | ||
| break; | ||
| case TextureStreamingMode::Balanced: | ||
| if (alphaCapable) | ||
| return 0; | ||
|
|
||
| desiredBias = maxDimension >= 4096 ? 2 : 1; | ||
| break; | ||
| case TextureStreamingMode::Aggressive: | ||
| desiredBias = maxDimension >= 4096 ? 3 : (maxDimension >= 2048 ? 2 : 1); | ||
| if (alphaCapable) | ||
| desiredBias = std::min(desiredBias, 1u); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
|
|
||
| desiredBias = std::min(desiredBias, settings.TextureMaxMipBias); | ||
|
|
||
| if (!IsBlockCompressedFormat(format)) | ||
| desiredBias = std::min(desiredBias, 1u); | ||
|
|
||
| return std::min(desiredBias, mipLevels - 1); | ||
| } |
There was a problem hiding this comment.
TextureBudgetMB is currently a no-op in streaming decisions.
The streaming bias path reads mode and max bias, but never uses ExperimentalSettings::TextureBudgetMB, so memory budget configuration doesn’t influence residency behavior.
Also applies to: 327-341
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Hooks.cpp` around lines 113 - 155, The GetTextureStreamingMipBias
function determines texture streaming bias but never uses the TextureBudgetMB
setting from ExperimentalSettings, making the memory budget configuration
ineffective. Integrate TextureBudgetMB into the bias calculation by computing an
estimated memory footprint for the texture at different mip levels, then adjust
the desiredBias upward as needed to keep memory consumption within the
configured budget limit. This adjustment should happen after the initial bias is
determined by TextureStreamingMode but before the final clamping operations,
ensuring the budget constraint overrides mode-based decisions when memory limits
would be exceeded.
This reverts commit e58b6c9. modified: shaders/raytracing/GlobalIllumination/RayGeneration.hlsl modified: shaders/raytracing/Pathtracing/RayGeneration.hlsl modified: shaders/raytracing/include/Common.hlsli modified: shaders/raytracing/include/Materials/BSDF.hlsli new file: shaders/raytracing/include/SampleGenerators.hlsli
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
shaders/raytracing/include/Common.hlsli (1)
1-1: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winSplit the
Random()calls before constructing the vectors. HLSL doesn’t guarantee argument evaluation order, sofloat2(Random(seed), Random(seed))and the similarfloat4(...)/float2(...)patterns can advance the shared seed in a compiler-dependent order. Use temporaries first inshaders/raytracing/include/Common.hlsliandshaders/raytracing/include/Materials/BSDF.hlsli.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@shaders/raytracing/include/Common.hlsli` at line 1, The Random() calls used to construct float2/float4 values can mutate the shared seed in an unspecified order. In the random-vector generation code in Common.hlsli and BSDF.hlsli, evaluate each Random(seed) call into separate temporaries first, then construct the vectors from those temporaries while preserving the existing component order and behavior.
🧹 Nitpick comments (1)
shaders/raytracing/include/Common.hlsli (1)
91-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
RandomduplicatesPCGHash's formula instead of calling it;PCGHashname is now stale.
PCGHash(seed)computesHash32(seed + kRandomStepSalt), andRandom's first line repeats that identical expression rather than callingPCGHash(seed). Also, since the body is nowHash32-based rather than a PCG hash, thePCGHashname is misleading to future readers/maintainers of either function.♻️ Proposed refactor
float Random(inout uint seed) { - seed = Hash32(seed + kRandomStepSalt); + seed = PCGHash(seed); return Hash32ToFloat(seed); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@shaders/raytracing/include/Common.hlsli` around lines 91 - 100, Update Random to reuse PCGHash(seed) for its seed transformation instead of duplicating the Hash32 expression, then rename PCGHash to a name that accurately reflects its Hash32-based behavior and update all references consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@shaders/raytracing/include/Common.hlsli`:
- Around line 148-151: Update the seed-based SampleCosineHemisphere overload to
generate the two random values in separate sequential assignments before
constructing the float2, ensuring deterministic .x/.y ordering while preserving
the existing sampling call.
In `@shaders/raytracing/include/Materials/BSDF.hlsli`:
- Around line 1130-1140: In the SampleBSDF overload that accepts randomSeed,
split each Random(randomSeed) invocation into sequential statements before
constructing preGeneratedSamples and extraSamples. Preserve the existing
six-call order and pass the resulting scalar values into the vectors so
randomSeed side effects are deterministic across compilers and targets.
---
Outside diff comments:
In `@shaders/raytracing/include/Common.hlsli`:
- Line 1: The Random() calls used to construct float2/float4 values can mutate
the shared seed in an unspecified order. In the random-vector generation code in
Common.hlsli and BSDF.hlsli, evaluate each Random(seed) call into separate
temporaries first, then construct the vectors from those temporaries while
preserving the existing component order and behavior.
---
Nitpick comments:
In `@shaders/raytracing/include/Common.hlsli`:
- Around line 91-100: Update Random to reuse PCGHash(seed) for its seed
transformation instead of duplicating the Hash32 expression, then rename PCGHash
to a name that accurately reflects its Hash32-based behavior and update all
references consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e44ba3fa-3557-45c8-a356-ce0506516256
⛔ Files ignored due to path filters (2)
shaders/raytracing/GlobalIllumination/RayGeneration.hlslis excluded by!**/*.hlslshaders/raytracing/Pathtracing/RayGeneration.hlslis excluded by!**/*.hlsl
📒 Files selected for processing (10)
shaders/raytracing/include/Common.hlslishaders/raytracing/include/Materials/BSDF.hlslishaders/raytracing/include/SampleGenerators.hlslisrc/API.cppsrc/API.hsrc/Core/Texture.hsrc/Core/TextureManager.cppsrc/Core/TextureManager.hsrc/Hooks.cppsrc/Types/Settings.h
🚧 Files skipped from review as they are similar to previous changes (6)
- src/Core/Texture.h
- src/API.cpp
- src/API.h
- src/Core/TextureManager.h
- src/Core/TextureManager.cpp
- src/Hooks.cpp
| float3 SampleCosineHemisphere(inout uint seed) | ||
| { | ||
| return SampleCosineHemisphere(float2(Random(seed), Random(seed))); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== File list =="
git ls-files 'shaders/raytracing/include/Common.hlsli' 'shaders/**/SampleGenerators.hlsli' 'shaders/**/BSDF.hlsli' || true
echo
echo "== Common.hlsli around relevant lines =="
sed -n '1,220p' shaders/raytracing/include/Common.hlsli
echo
echo "== Search for Random(seed) constructor patterns =="
rg -n "Random\\(seed\\).*Random\\(seed\\)|float2\\(Random\\(seed\\), Random\\(seed\\)\\)|SampleCosineHemisphere\\(|SampleConeUniform\\(|UniformSampleNext2D|SampleSequenceNext2D" shaders -S || trueRepository: Gistix/CreationEngineRaytracing
Length of output: 7073
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path('shaders/raytracing/include/Common.hlsli')
text = path.read_text()
for needle in [
'float3 SampleCosineHemisphere(inout uint seed)',
'float3 SampleConeUniform(inout uint seed',
'uint Random(inout uint seed)',
'float2 UniformSampleNext2D',
'float2 SampleSequenceNext2D',
]:
idx = text.find(needle)
print(f'-- {needle} --')
if idx == -1:
print('not found')
continue
start = max(0, text.rfind('\n', 0, idx-200))
end = min(len(text), text.find('\n\n', idx+1))
print(text[idx: end if end != -1 else idx+600])
print()
PYRepository: Gistix/CreationEngineRaytracing
Length of output: 534
🌐 Web query:
HLSL function argument evaluation order unspecified inout parameter Random(seed)
💡 Result:
In HLSL, the order of function argument evaluation is unspecified, as it depends on the underlying compiler and the C++ ABI implementation [1][2][3]. Similar to C++, HLSL does not define a fixed order for parameter initialization or destruction [2][4]. For inout parameters, the behavior can vary based on the specific ABI in use [2]. For instance, in compilers using MSVC compatibility modes, arguments may be emitted right-to-left, whereas other ABIs (such as Itanium) may use a reversed order [2][5]. Because this order is not guaranteed by the language specification, relying on a specific evaluation order for side effects—such as multiple modifications to the same variable within a single function call—is unsafe and can lead to non-portable or undefined behavior [2]. Regarding the Random(seed) pattern frequently seen in HLSL, it is standard practice to pass the seed by inout to maintain the state of the pseudo-random number generator (PRNG) across multiple calls [6]. The typical implementation, such as Xorshift, modifies the seed variable directly [6]: uint Random(inout uint state) { state ^= (state << 13); state ^= (state >> 17); state ^= (state << 5); return state; } Because the order of argument evaluation is unspecified, you should avoid passing the same variable to multiple inout parameters in a single function call (e.g., Func(inout x, inout x)). Doing so relies on implementation-defined behavior regarding how and when these parameters are written back to the original argument [2][7]. Instead, perform such operations sequentially in separate statements to ensure predictable results.
Citations:
- 1: https://clang.llvm.org/docs/HLSL/FunctionCalls.html
- 2: https://releases.llvm.org/21.1.0/tools/clang/docs/HLSL/FunctionCalls.html
- 3: https://releases.llvm.org/18.1.7/tools/clang/docs/HLSL/FunctionCalls.html
- 4: https://releases.llvm.org/19.1.0/tools/clang/docs/HLSL/FunctionCalls.html
- 5: https://rocm.docs.amd.com/projects/llvm-project/en/docs-6.4.1/LLVM/clang/html/HLSL/FunctionCalls.html
- 6: https://github.com/microsoft/DirectX-Graphics-Samples/blob/master/Samples/Desktop/D3D12Raytracing/src/D3D12RaytracingRealTimeDenoisedAmbientOcclusion/RTAO/Shaders/RandomNumberGenerator.hlsli
- 7: [HLSL][Docs] Add documentation for HLSL functions llvm/llvm-project#75397
Split the two Random(seed) calls
float2(Random(seed), Random(seed)) mutates the same inout seed twice inside one expression. HLSL doesn’t guarantee argument evaluation order, so which random lands in .x vs .y can vary by compiler/backend. Use two sequential assignments before calling SampleCosineHemisphere(...).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shaders/raytracing/include/Common.hlsli` around lines 148 - 151, Update the
seed-based SampleCosineHemisphere overload to generate the two random values in
separate sequential assignments before constructing the float2, ensuring
deterministic .x/.y ordering while preserving the existing sampling call.
| bool SampleBSDF(const BRDFContext brdfContext, const LightingMaterialData material, const Surface surface, out BSDFSample result, inout uint randomSeed) | ||
| { | ||
| return SampleBSDF(brdfContext, material.Feature, surface, result, randomSeed); | ||
| float4 preGeneratedSamples = float4( | ||
| Random(randomSeed), | ||
| Random(randomSeed), | ||
| Random(randomSeed), | ||
| Random(randomSeed) | ||
| ); | ||
| float2 extraSamples = float2(Random(randomSeed), Random(randomSeed)); | ||
| return SampleBSDF(brdfContext, material.Feature, surface, result, preGeneratedSamples, extraSamples); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant file and the related helper mentioned in the review.
git ls-files 'shaders/raytracing/include/Materials/BSDF.hlsli' 'shaders/**/Common.hlsli' 'shaders/**/Common.hlsli' | sed 's#^`#FILE` #'
echo
echo "== BSDF.hlsli context =="
sed -n '1120,1145p' shaders/raytracing/include/Materials/BSDF.hlsli
echo
echo "== Common.hlsli search for the same pattern =="
rg -n "Random\\(randomSeed\\)" shaders -g '*.hlsli'
echo
echo "== Common.hlsli context =="
common_file="$(rg -l "SampleCosineHemisphere\\(inout uint\\)|Random\\(randomSeed\\)" shaders -g 'Common.hlsli' | head -n 1 || true)"
if [ -n "${common_file:-}" ]; then
sed -n '130,170p' "$common_file"
fiRepository: Gistix/CreationEngineRaytracing
Length of output: 4541
🌐 Web query:
HLSL argument evaluation order constructor function call random seed unspecified evaluation order Microsoft documentation
💡 Result:
In HLSL, the order in which function arguments are evaluated is unspecified [1]. While HLSL specifications mandate that argument expressions are evaluated before the function call occurs [2], the language does not define a specific order for the evaluation of those expressions [1]. Because this evaluation order is unspecified, code that relies on a specific sequence of side effects among arguments (such as expressions involving a random number generator or other state-changing functions) is considered non-portable and potentially dangerous [3]. Relying on a particular order can lead to inconsistent behavior across different compilers, shader models, or targets, as different implementation backends (such as those following MSVC or Itanium C++ ABI conventions) may evaluate and initialize parameters in different orders [3][4]. To ensure predictable and correct behavior, especially when using functions that produce side effects or rely on state (like random seed updates), you should explicitly separate these operations into distinct statements before the function call [1]. Top results: [3], [2], [4], [1]
Citations:
- 1: WGSL should specify the execution order gpuweb/gpuweb#2261
- 2: https://microsoft.github.io/hlsl-specs/specs/hlsl.pdf
- 3: https://clang.llvm.org/docs/HLSL/FunctionCalls.html
- 4: https://releases.llvm.org/21.1.0/tools/clang/docs/HLSL/FunctionCalls.html
Split the RNG calls before constructing the sample vectors. HLSL leaves argument evaluation order unspecified, so Random(randomSeed) side effects inside float4(...) and float2(...) can produce different sample sequences across compilers and targets.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shaders/raytracing/include/Materials/BSDF.hlsli` around lines 1130 - 1140, In
the SampleBSDF overload that accepts randomSeed, split each Random(randomSeed)
invocation into sequential statements before constructing preGeneratedSamples
and extraSamples. Preserve the existing six-call order and pass the resulting
scalar values into the vectors so randomSeed side effects are deterministic
across compilers and targets.
Summary by CodeRabbit