#pragma only_renderers d3d11 vulkan metal glcore
// This shader computes a strided segmented reduction - essentially N reductions in parallel, in a single pass.
// A very simple approach is used, where each thread just computes the i'th reduction sequentially.
// Each segment is assumed to be the same length. The shader adds to its output, so zero-initialize it if needed.

// Example with width = 2, stride = 3:
// 
// Input buffer layout:
// |val 0|val 1|val 2|val 3|val 4|val 5|val 6|val 7|val 8|val 9|val 10|val 11|...
// |    element 0    |    element 1    |    element 2    |     element 3     |...
// |             segment 0             |             segment 1               |...
//
// Resulting output buffer:
// |val 0 + val 3|val 1 + val 4|val 2 + val 5|val 6 + val 9|val 7 + val 10|val 8 + val 11|...
// |            element 0 + element 1        |            element 2 + element 3          |...

#pragma kernel SegmentedReductionFloat

#define MAX_SEGMENT_STRIDE 32

uint g_SegmentWidth;    // How many elements in each segment
uint g_SegmentStride;   // How many values in each element
uint g_SegmentCount;    // How many segments
uint g_InputOffset;     // Offset into input buffer, specified in elements
uint g_OutputOffset;    // Offset into output buffer, specified in elements
uint g_OverwriteOutput; // If 1, overwrite the output buffer. Otherwise, add to it.
StructuredBuffer<float> g_InputFloatBuffer;    // Length = g_SegmentWidth * g_SegmentStride * g_NumSegments
RWStructuredBuffer<float> g_OutputFloatBuffer; // Length = g_SegmentStride * g_NumSegments

// These uniforms can be used for multi-pass reductions, handling cases where the input segments can
// not be cleanly divided into sub-segments. If g_TruncateInterval > 0 && g_TruncatedSegmentWidth > 0,
// every g_TruncateInterval'th segment will be truncated to a width given by g_TruncatedSegmentWidth.
uint g_TruncateInterval;
uint g_TruncatedSegmentWidth;

[numthreads(64,1,1)]
void SegmentedReductionFloat(uint3 id : SV_DispatchThreadID)
{
	if (id.x >= g_SegmentCount)
		return;

	uint currentSegmentWidth = g_SegmentWidth; // How large is this threads segment?
	uint truncatedElementsThusFar = 0; // How many elements have been truncated thus far?

	// Handle truncation
	bool truncationEnabled = g_TruncateInterval > 0 && g_TruncatedSegmentWidth > 0;
	if (truncationEnabled)
	{
		truncatedElementsThusFar = (id.x / g_TruncateInterval) * (g_SegmentWidth - g_TruncatedSegmentWidth);

		bool shouldTruncate = (id.x + 1) % g_TruncateInterval == 0;
		if (shouldTruncate)
		{
			currentSegmentWidth = g_TruncatedSegmentWidth;
		}
	}

	// Step 1: Zero initialize an accumulator in local memory
	float accumulator[MAX_SEGMENT_STRIDE];
	uint valueIdx;
	for (valueIdx = 0; valueIdx < g_SegmentStride; valueIdx++)
	{
		accumulator[valueIdx] = 0.0f;
	}

	// Step 2: Accumulate values in the segment into local memory
	const uint baseElementIndex = id.x * g_SegmentWidth - truncatedElementsThusFar + g_InputOffset;
	for (uint elementIdx = 0; elementIdx < currentSegmentWidth; elementIdx++)
	{
		for (valueIdx = 0; valueIdx < g_SegmentStride; valueIdx++)
		{
			accumulator[valueIdx] += g_InputFloatBuffer[(baseElementIndex + elementIdx) * g_SegmentStride + valueIdx];
		}
	}

	// Step 3: Write accumulated values to global memory
	for (valueIdx = 0; valueIdx < g_SegmentStride; valueIdx++)
	{
		if (g_OverwriteOutput)
		{
			g_OutputFloatBuffer[(id.x + g_OutputOffset) * g_SegmentStride + valueIdx] = accumulator[valueIdx];
		}
		else
		{
			g_OutputFloatBuffer[(id.x + g_OutputOffset) * g_SegmentStride + valueIdx] += accumulator[valueIdx];
		}
	}
}
