HDRI radiance, half-float storage and GGX termination
Read-only CPU/source review, 17 September 2026. No GPU workload, import or asset edit. The proposed simple chain “AlpineDay is bright, therefore an infinity makes GGX loop forever” is not supported by the inspected data and shaders.
Original source values
The existing Assets/Rampage/Resources/Skies/AlpineDay.hdr was decoded on CPU as Radiance RGBE scanline RLE, with RGB = channel * 2^(exponent-136), zero for exponent zero. No third-party image transformation, exposure adjustment or write to the asset was performed.
| Quantity | Result |
|---|---|
| SHA-256 | 462a3cc63bfcd9fe0299137a0f33f4ade84e237f4cb6115fe46f5ab400693fe6 |
| Size | 2048 x 1024 |
| All source RGB values finite / nonnegative | true / true |
| Maximum R / G / B | 114176 / 110592 / 104960 |
| Maximum Rec.709 luminance | 110947.328 |
| Runtime lux multiplier | 78654.18 / 5.45773304 = 14411.511047451304 |
| Largest source component times multiplier | 1645448685.3538, finite in float32 |
| Original panorama pixels above 65504 after this multiplication | 1252 of 2097152 |
These are original panorama measurements, not the BC6H-compressed, resampled cubemap or a GPU readback. They reproduce the prior maximum-luminance measurement in Docs/ALPINE-SKY-DIAGNOSIS.md. SkyImportSettings.cs imports linear BC6H cubemaps with 512-pixel faces and mipmaps; the source is not CPU-readable after Unity import. Import compression/resampling and sky-cube sampling may change which texels saturate.
Where the actual path limits radiance
Pinned HDRP 17.3 Runtime/Sky/HDRISky/HDRISky.shader:213–218 uses an explicitly float3 product of cubemap color, intensity and exposure, then ClampToFloat16Max(skyColor) before returning the fragment output. Core Common.hlsl:1700 defines this as min(value, HALF_MAX); HALF_MAX is 65504.
The cubemap baking fragment at lines 265–268 calls RenderSky with exposure 1.0. The camera fragment instead uses GetCurrentExposureMultiplier(). Camera EV13 therefore does not rescue the unexposed lighting cube; the explicit shader clamp is the relevant protection. SkyRenderingContext.cs:37,41 and SkyManager.cs:933 allocate the sky and GGX intermediate resources as R16G16B16A16_SFloat, not packed R11G11B10. For the measured finite, nonnegative source and finite multiplier, the shown arithmetic does not create infinity before the clamp and cannot overflow half storage merely because of the source radiance.
Bright-sun saturation can alter lighting fidelity. That is different from observing non-finite GPU values or proving a TDR mechanism. This review does not inspect every downstream renderer computation and does not claim general shader memory safety.
GGX loops: distinguish filtering from sample-table preparation
- Ordinary sky filtering calls
IBLFilterGGX.FilterCubemap, not FilterCubemapMIS (SkyManager.cs:943). It copies six faces then loops over six nonzero mips/six faces.GGXConvolve.shadercalls IntegrateLD with fixed runtime counts of 21, 34, 55, 89, 89, 89.ImageBasedLighting.hlsl:440loops over that count, independently of sampled radiance. Accumulated light and weight use explicit float variables. High color values do not change the loop bound. - The alternate MIS code has a fixed 1024-sample loop. Its binary search advances by right-shifting an integer bit (
b >>= 1); a NaN comparison does not turn it into a radiance-dependent unbounded loop. No caller of FilterCubemapMIS was found in the inspected HDRP runtime outside its method definitions. - One preparation loop is genuinely unbounded in source:
ComputeGgxIblSampleData.compute:48–76useswhile(true), increasing candidate sampleCount until the requested number of upper-hemisphere samples is accepted. This kernel reads no HDRI pixels. Its inputs are fixed mip indices 1–6, roughness derived from those indices, and the Golden sequence. It initializes the 89x6 sample lookup texture, with reinitialization only if that resource is not created. It is not a radiance-dependent loop repeated by each convolution texel. - A CPU float32 evaluation of the shown Golden-sequence/GGX acceptance equations terminated for all six mips at candidate counts 21, 34, 56, 94, 108, 178 (at most 90 increments/checks from the initial count). All tested square roots/divisions were finite. This is a numerical CPU cross-check, not proof of compiler/driver behavior or GPU execution. The fixed last mip saturates perceptual roughness at one; sub-ULP differences in CPU expression evaluation do not establish bit-identical shader execution.
- AmbientProbeConvolution uses exactly 256 threads and 27 coefficients, with bounded reduction loops. Its structured buffers store float/uint bit representations, not accumulated half values.
Exact lifetime of the unbounded preparation loop
HDRenderPipeline.cs:658–669 constructs one IBLFilterGGX for the pipeline and passes the same filter array to SkyManager. During rendering, HDRenderPipeline.cs:2913–2916 calls Initialize only if IsInitialized is false. IBLFilterGGX.cs:38–40 defines initialized as a non-null lookup RenderTexture; lines 63–74 allocate that 89x6 texture and dispatch ComputeGgxIblSampleData once. Lines 92–95 bind only its output texture and dispatch exactly one group. There are no caller-supplied sky-lux, HDRI-color, sample-count or roughness constant-buffer values for this kernel: mip and sample counts derive from thread indices and compile-time functions in the shader.
Subsequent Realtime sky updates use that existing lookup. FilterCubemapCommon:161–165 redispatches preparation only when the existing RenderTexture reports IsCreated()==false, after recreating it. Pipeline cleanup destroys the lookup (HDRenderPipeline.cs:1046–1048, IBLFilterGGX.cs:133–136). Normal scene replacement does not construct another filter or deliberately release this texture in the inspected application code.
Thus “the while(true) runs afresh on every one-second sky update” is false. Startup/reinitialization is the relevant scope. A successfully rendered garage and multiple alps loads before failure lower the priority of an ordinary deterministic LUT-initialization hang. A device/resource loss could cause the reinitialization branch, but that can be a consequence of the reset; no evidence shows it initiating the current incident. No loop patch is justified.
17.3 versus 17.6 and next action
SHA-256 comparisons show byte-identical copies of HDRISky.shader, GGXConvolve.shader, AmbientProbeConvolution.compute and ComputeGgxIblSampleData.compute between the current 17.3 package and the installed 6.6 Editor's 17.6 package. The fresh engine port may still change native submission or other package code; these particular shaders do not supply a claimed overflow fix.
Do not add a NaN guard, reduce the HDRI multiplier or alter the sky asset on this evidence. The separate periodic sky/ambient/convolution lifetime hypothesis remains testable by changing indirect-sky updates alone after the current-content engine comparison. That experiment targets recurring work/resource behavior, not a demonstrated numeric overflow.