Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e69110c
Add files via upload
KthanidKisure Aug 27, 2026
eb6e1af
Merge pull request #2 from KthanidKisure/KthanidKisure-patch-2
KthanidKisure Aug 27, 2026
5ac44ae
Add files via upload
KthanidKisure Aug 27, 2026
2070112
Merge pull request #3 from KthanidKisure/KthanidKisure-patch-3
KthanidKisure Aug 27, 2026
d8d0c82
Add files via upload
KthanidKisure Aug 27, 2026
3869726
Merge pull request #4 from KthanidKisure/KthanidKisure-patch-4
KthanidKisure Aug 27, 2026
abeec9e
Add files via upload
KthanidKisure Aug 27, 2026
e841108
Merge pull request #5 from KthanidKisure/KthanidKisure-patch-5
KthanidKisure Aug 27, 2026
01a5434
Add files via upload
KthanidKisure Aug 27, 2026
cabec16
Merge pull request #6 from KthanidKisure/KthanidKisure-patch-6
KthanidKisure Aug 27, 2026
e07c167
Add files via upload
KthanidKisure Aug 27, 2026
3345569
Merge pull request #7 from KthanidKisure/KthanidKisure-patch-7
KthanidKisure Aug 27, 2026
ec6e916
Add files via upload
KthanidKisure Aug 27, 2026
4bede59
Merge pull request #8 from KthanidKisure/KthanidKisure-patch-8
KthanidKisure Aug 27, 2026
b250341
Add files via upload
KthanidKisure Aug 28, 2026
89c2963
Merge pull request #9 from KthanidKisure/KthanidKisure-patch-9
KthanidKisure Aug 28, 2026
8142e72
Add files via upload
KthanidKisure Aug 28, 2026
2f48db6
Merge pull request #10 from KthanidKisure/KthanidKisure-patch-10
KthanidKisure Aug 28, 2026
4fc19aa
Add files via upload
KthanidKisure Aug 28, 2026
3051a1e
Merge pull request #11 from KthanidKisure/KthanidKisure-patch-11
KthanidKisure Aug 28, 2026
e528be2
Add files via upload
KthanidKisure Aug 28, 2026
fd43883
Merge pull request #12 from KthanidKisure/KthanidKisure-patch-12
KthanidKisure Aug 28, 2026
b0c340a
Add files via upload
KthanidKisure Aug 28, 2026
e8922e8
Merge pull request #13 from KthanidKisure/KthanidKisure-patch-13
KthanidKisure Aug 28, 2026
62aefcd
Add files via upload
KthanidKisure Aug 28, 2026
1fdb931
Merge pull request #14 from KthanidKisure/KthanidKisure-patch-14
KthanidKisure Aug 28, 2026
af2ead4
Add files via upload
KthanidKisure Aug 28, 2026
eab0c54
Merge pull request #15 from KthanidKisure/KthanidKisure-patch-15
KthanidKisure Aug 28, 2026
775fb91
Add files via upload
KthanidKisure Aug 28, 2026
cd3392b
Add files via upload
KthanidKisure Aug 28, 2026
280860c
Merge pull request #16 from KthanidKisure/KthanidKisure-patch-16
KthanidKisure Aug 28, 2026
ac7f2e9
Add files via upload
KthanidKisure Aug 29, 2026
fa9d0e1
Merge pull request #17 from KthanidKisure/KthanidKisure-patch-17
KthanidKisure Aug 29, 2026
10fbf36
Add files via upload
KthanidKisure Aug 29, 2026
4673f1e
Merge pull request #18 from KthanidKisure/KthanidKisure-patch-18
KthanidKisure Aug 29, 2026
a983f1e
Add files via upload
KthanidKisure Aug 30, 2026
a73198e
Merge pull request #19 from KthanidKisure/KthanidKisure-patch-19
KthanidKisure Aug 30, 2026
43bb704
Add one-shot workflow for RT initialization fix
KthanidKisure Aug 31, 2026
80d900a
Fix one-shot RT patch workflow
KthanidKisure Aug 31, 2026
73abdbc
Remove one-shot workflow bootstrap
KthanidKisure Aug 31, 2026
7a42ee1
Temporarily bootstrap RT initialization source fix
KthanidKisure Aug 31, 2026
8e8afc0
Fix cloud noise pipeline initialization
github-actions[bot] Aug 31, 2026
1451ea9
Restore standard CI workflow
KthanidKisure Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ abstract class GenerateRtBindings extends DefaultTask {
TLAS: "topLevelAS", OUTPUT: "outImage", BLOCK_ALBEDO: "blockAlbedoAtlas",
G_NORMAL: "gNormal", G_ALBEDO: "gAlbedo", G_DEPTH: "gDepth", G_MOTION: "gMotion",
G_SPEC_ALBEDO: "gSpecAlbedo", G_SPEC_MOTION: "gSpecMotion",
RESERVOIR_A: "reservoirA", RESERVOIR_B: "reservoirB",
CLOUD_NOISE: "cloudNoiseTex",
CELESTIALS: "celestialsAtlas", SKY_VIEW: "skyViewLut", TRANSMITTANCE: "transmittanceLut",
ENTITY_ALBEDO: "entityAlbedoTex", MATERIAL_SURFACE0: "materialSurface0Tex",
MATERIAL_NORMAL_AO: "materialNormalAoTex", MATERIAL_SURFACE1: "materialSurface1Tex"]],
Expand Down Expand Up @@ -120,9 +122,15 @@ abstract class GenerateRtBindings extends DefaultTask {
constants.WORLD_SET = ordinary.values().first().set
ordinary.each { suffix, location -> constants["WORLD_${suffix}"] = location.index }
def guides = ordinary.findAll { suffix, ignored -> (suffix as String).startsWith("G_") }
def storageImages = guides + ordinary.findAll { suffix, ignored -> suffix == "OUTPUT" }
// RESERVOIR_* are storage images like the guides, but deliberately not counted as guides:
// WORLD_GUIDE_COUNT bounds setExtraStorageImage's slot range, which addresses bindings
// contiguously from WORLD_G_NORMAL. Folding the reservoirs in there would let a guide
// slot index walk into them.
def reservoirs = ordinary.findAll { suffix, ignored -> (suffix as String).startsWith("RESERVOIR_") }
def storageImages = guides + reservoirs + ordinary.findAll { suffix, ignored -> suffix == "OUTPUT" }
def samplers = ordinary.findAll { suffix, ignored -> suffix != "TLAS" && !storageImages.containsKey(suffix) }
constants.WORLD_GUIDE_COUNT = guides.size()
constants.WORLD_RESERVOIR_COUNT = reservoirs.size()
constants.WORLD_SET_BINDING_COUNT = ordinary.values()*.index.max() + 1
constants.WORLD_SET_STORAGE_IMAGE_COUNT = storageImages.size()
constants.WORLD_SET_SAMPLER_COUNT = samplers.size()
Expand Down
13 changes: 13 additions & 0 deletions shaders/common/display_common.slang
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ public struct DisplayPush {
// SUM of every band, so RtComposite folds the 1/levelCount normalisation into this value: the authored
// look-package strength then means the same thing whichever level count the resolution supports.
public float bloomStrength;
// ---- Unreal-style scene-referred colour grading, applied after the LMT and before the output
// transform, which is where UE's own grade sits relative to its tonemapper. Values are the
// renderer's working space (ACEScg/AP1) and UE's default working space is also AP1, so the
// published numbers from a UE post-process volume transfer directly rather than approximately.
public int gradeEnabled; // 0 = skip the whole stage
public float saturation; // global
public float contrast; // global, pivoted at 0.18
public float gain; // global
public float highlightSaturation; // multiplies the global value in the highlight region
public float highlightGain; // multiplies the global value in the highlight region
public float highlightsMin; // luma where the highlight region starts blending in
// ---- RCAS. 0 disables it and its four extra taps.
public float sharpness;
};

public struct BloomPush {
Expand Down
128 changes: 122 additions & 6 deletions shaders/pipelines/display/main.comp.slang
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,56 @@ float3 applyLook(float3 exposedAcesCg) {
return shaperDecode(shaped);
}

// ---- Unreal-style colour grading -----------------------------------------------------------------
//
// A reimplementation of UE's ColorCorrectAll from PostProcessCombineLUTs.usf, so that a grade authored
// against a UE post-process volume — such as the one in the "Ultra Realism Tonemapper" guide — produces
// the same image here. Two things make that transfer exact rather than approximate: UE's default
// working colour space is AP1, which is this renderer's working space too, and UE grades scene-referred
// before its tonemapper, which is where this sits relative to the output transform.
//
// The 0.18 contrast pivot is what keeps contrast from also being an exposure change: scaling around
// mid grey leaves mid grey fixed, so contrast and exposure stay independent controls.
// ACEScg luma weights, matching world_common.slang's ACESCG_LUMA. Duplicated rather than imported:
// this pipeline does not otherwise depend on the ray tracer's module tree.
static const float3 ACESCG_LUMA = float3(0.27222872, 0.67408177, 0.05368952);
static const float GRADE_PIVOT = 0.18;
// UE's ColorCorrectionShadowsMax / HighlightsMax defaults. Only HighlightsMin is worth exposing —
// it is the one the guide actually moves, and the other two only reshape the blend between regions.
static const float GRADE_SHADOWS_MAX = 0.09;
static const float GRADE_HIGHLIGHTS_MAX = 1.0;

float3 colorCorrect(float3 color, float saturation, float contrast, float gain) {
float luma = dot(color, ACESCG_LUMA);
// Saturation as a lerp from luma, clamped at zero: values above 1 extrapolate away from grey, and
// without the clamp a strongly saturated colour can extrapolate a channel negative and come back
// through the tone LUT as a hard chroma artifact.
color = max(lerp(float3(luma), color, saturation), float3(0.0));
color = pow(max(color / GRADE_PIVOT, float3(0.0)), float3(contrast)) * GRADE_PIVOT;
return color * gain;
}

float3 applyGrade(float3 acesCg) {
if (pc.gradeEnabled == 0) {
return acesCg;
}
float luma = dot(acesCg, ACESCG_LUMA);
// Region weights sum to 1 by construction, so the three graded results blend without changing
// overall level — the midtone weight is whatever the other two leave behind.
float shadowWeight = 1.0 - smoothstep(0.0, GRADE_SHADOWS_MAX, luma);
float highlightWeight = smoothstep(pc.highlightsMin, GRADE_HIGHLIGHTS_MAX, luma);
float midWeight = max(1.0 - shadowWeight - highlightWeight, 0.0);

// Region parameters multiply the global ones, exactly as UE composes them — so the guide's
// highlight saturation of 0.95 means 0.95 x the global 0.75, not 0.95 outright.
float3 midtones = colorCorrect(acesCg, pc.saturation, pc.contrast, pc.gain);
float3 highlights = colorCorrect(acesCg, pc.saturation * pc.highlightSaturation,
pc.contrast, pc.gain * pc.highlightGain);
// Shadows use the global grade unmodified: the guide overrides no shadow-region parameters, and
// inventing a shadow lift here would silently diverge from the source it is reproducing.
return midtones * (shadowWeight + midWeight) + highlights * highlightWeight;
}

float3 sampleBloom(int2 outputPixel, uint outputWidth, uint outputHeight) {
float2 uv = (float2(outputPixel) + 0.5) / float2(outputWidth, outputHeight);
return bloomImage.SampleLevel(uv, 0.0).rgb;
Expand Down Expand Up @@ -141,6 +191,55 @@ float3 tonemapHdr(float3 lookedAcesCg) {
return displayGammaHdr(hdrToneLut.SampleLevel(lutTexCoord(uvw, pc.lutSize), 0.0).rgb);
}

// Everything up to, but not including, the output transform. Factored out because RCAS below needs it
// at four neighbouring pixels as well as the centre.
float3 gradedAcesCg(int2 pix, float exposure, uint w, uint h) {
float3 exposedAcesCg = max(rtImage[pix].rgb * exposure, float3(0.0));
exposedAcesCg += sampleBloom(pix, w, h) * max(pc.bloomStrength, 0.0);
return applyGrade(applyLook(exposedAcesCg));
}

// ---- RCAS ------------------------------------------------------------------------------------------
//
// AMD's Robust Contrast-Adaptive Sharpening, from FSR. "Robust" is the operative word: rather than a
// fixed unsharp kernel, it derives a per-pixel sharpening lobe from how much headroom the local
// neighbourhood actually has, so it cannot ring a highlight into clipping or crush a shadow. That makes
// it the right choice downstream of DLSS Ray Reconstruction, where a fixed sharpen would amplify the
// denoiser's own reconstruction error along edges.
//
// TWO DELIBERATE PLACEMENT DECISIONS:
//
// It runs AFTER the output transform, on display code values, not on scene-linear radiance. RCAS's
// headroom maths assumes a bounded signal with a peak of 1. Scene-linear HDR has no such bound — a
// 10,000 nit emitter is just a large number — and sharpening it would produce overshoot proportional to
// the highlight's absolute intensity rather than to its visible contrast.
//
// The HDR path sharpens PQ code values, which is what "HDR-aware" means here. PQ is perceptually
// uniform and bounded [0,1], so the same lobe produces the same *apparent* edge enhancement at 100 nits
// and at 1000 nits. Sharpening linear nits instead would make bright highlights receive hundreds of
// times more absolute overshoot than midtones, which reads as ringing exactly where the display is most
// able to show it.
//
// Cost is four extra pipeline evaluations per pixel. Gated on sharpness, so a disabled slider costs one
// comparison.
static const float RCAS_LIMIT = -0.1875; // FSR's clamp on the lobe; beyond it the filter visibly rings.

float3 rcas(float3 e, float3 b, float3 d, float3 f, float3 h, float sharpness) {
float3 mn4 = min(min(b, d), min(f, h));
float3 mx4 = max(max(b, d), max(f, h));
// Headroom to black and to peak white, per channel. The tighter of the two bounds the lobe, so a
// pixel already near either end of the range is sharpened less rather than pushed past it.
float3 hitMin = mn4 / (4.0 * max(mx4, float3(1.0e-6)));
float3 hitMax = (1.0 - mx4) / (4.0 * max(mx4, float3(1.0e-6)));
float3 lobeRgb = max(-hitMin, hitMax);
float lobe = max(RCAS_LIMIT, min(max(lobeRgb.r, max(lobeRgb.g, lobeRgb.b)), 0.0)) * sharpness;
return (lobe * (b + d + f + h) + e) / (4.0 * lobe + 1.0);
}

int2 clampPixel(int2 pix, uint w, uint h) {
return int2(clamp(pix.x, 0, int(w) - 1), clamp(pix.y, 0, int(h) - 1));
}

[shader("compute")]
[numthreads(16, 16, 1)]
void main(uint3 dispatchId : SV_DispatchThreadID) {
Expand All @@ -151,14 +250,31 @@ void main(uint3 dispatchId : SV_DispatchThreadID) {
return;
}

float4 rt = rtImage[pix];
float exposure = max(exposureImage[int2(0, 0)], 0.0);
float3 exposedAcesCg = max(rt.rgb * exposure, float3(0.0));
exposedAcesCg += sampleBloom(pix, w, h) * max(pc.bloomStrength, 0.0);
float3 lookedAcesCg = applyLook(exposedAcesCg);
outputImage[pix] = float4(tonemap(lookedAcesCg), 1.0);
float3 lookedAcesCg = gradedAcesCg(pix, exposure, w, h);

float sharpness = max(pc.sharpness, 0.0);
if (sharpness <= 0.0) {
outputImage[pix] = float4(tonemap(lookedAcesCg), 1.0);
if (pc.hdrEnabled != 0) {
hdrImage[pix] = float4(tonemapHdr(lookedAcesCg), 1.0);
}
return;
}

// Edge pixels clamp to themselves, which makes the lobe collapse toward zero at the border rather
// than sharpening against a wrapped or undefined neighbour.
float3 upAcesCg = gradedAcesCg(clampPixel(pix + int2(0, -1), w, h), exposure, w, h);
float3 leftAcesCg = gradedAcesCg(clampPixel(pix + int2(-1, 0), w, h), exposure, w, h);
float3 rightAcesCg = gradedAcesCg(clampPixel(pix + int2(1, 0), w, h), exposure, w, h);
float3 downAcesCg = gradedAcesCg(clampPixel(pix + int2(0, 1), w, h), exposure, w, h);

outputImage[pix] = float4(rcas(tonemap(lookedAcesCg), tonemap(upAcesCg), tonemap(leftAcesCg),
tonemap(rightAcesCg), tonemap(downAcesCg), sharpness), 1.0);

if (pc.hdrEnabled != 0) {
hdrImage[pix] = float4(tonemapHdr(lookedAcesCg), 1.0);
hdrImage[pix] = float4(rcas(tonemapHdr(lookedAcesCg), tonemapHdr(upAcesCg),
tonemapHdr(leftAcesCg), tonemapHdr(rightAcesCg),
tonemapHdr(downAcesCg), sharpness), 1.0);
}
}
8 changes: 7 additions & 1 deletion shaders/pipelines/world/any_hit.rahit.slang
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ void main(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr)
int texSlot = int(epr.tint.w + 0.5);
float4 texel = entityAlbedoTex[NonUniformResourceIndex(texSlot)].SampleLevel(uv, 0.0);
bool stochasticAlpha = false;
MaterialHeader materialHeader;
// Zero-initialised rather than left to the ENTITY_BIT branch below. Every read of this is
// already guarded by an instanceKind == ENTITY_BIT test, so the value is never observed
// unassigned -- but the compiler cannot prove that across the separate guards, and newer slangc
// versions promote the resulting may-be-uninitialised diagnostic to an error under the
// project's -warnings-as-errors. A defined zero is also the safer failure mode: model 0 falls
// through the dielectric and water branches instead of reading a stack value as a material.
MaterialHeader materialHeader = {};
if (instanceKind == ENTITY_BIT) {
materialHeader = ConstPtr<MaterialHeader>(pc.materialTableAddr)[epr.materialId];
stochasticAlpha = (materialHeader.features & MATERIAL_FEATURE_STOCHASTIC_ALPHA) != 0u;
Expand Down
13 changes: 13 additions & 0 deletions shaders/pipelines/world/bindings.slang
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ import world_common;
[[vk::binding(7, 0)]] [format("rgba16f")] public RWTexture2D<float4> gSpecAlbedo;
[[vk::binding(8, 0)]] [format("rg16f")] public RWTexture2D<float2> gSpecMotion;

// ReSTIR temporal reservoirs. Both are bound permanently and swap roles each frame via
// worldPush.restir.y, rather than the descriptors themselves being rewritten: descriptor sets are
// ring-buffered across frames in flight, so a per-frame rewrite would mutate a set an earlier frame is
// still reading. A push constant costs nothing and cannot race.
// Double-width — two texels per pixel, see restir.slang. rgba32f because the emitter position needs
// full float precision.
[[vk::binding(12, 0)]] [format("rgba32f")] public RWTexture2D<float4> reservoirA;
[[vk::binding(13, 0)]] [format("rgba32f")] public RWTexture2D<float4> reservoirB;

// Precomputed cloud shape field. R = 4-octave base, G = 3-octave erosion detail, tiling in all three
// axes. Replaces hundreds of ALU ops per density sample with one filtered fetch — see RtCloudNoise.
[[vk::binding(14, 0)]] public Sampler3D cloudNoiseTex;

[[vk::binding(2, 0)]] public Sampler2D blockAlbedoAtlas;
[[vk::binding(9, 0)]] public Sampler2D celestialsAtlas;
[[vk::binding(10, 0)]] public Sampler2D skyViewLut;
Expand Down
Loading