Graphics glossary 38 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
Anti-aliasing does not appear because Vulkan uses more threads. It appears when the renderer asks more than one question inside a pixel and combines the answers correctly. Lesson 13 established that principle by hand. The compute renderer must now give every sample a reproducible position, preserve a running estimate in linear colour and discard history whenever the scene or camera changes.
The accumulation image and display image must remain separate. The former stores floating-point history; the latter contains the current encoded presentation result. Treating an 8-bit display pixel as the running total would lose information at every update.
What you should be able to account for
- Generate a deterministic sub-pixel offset from pixel coordinates and sample index.
- Update a floating-point running sum and count without applying display encoding to stored history.
- State which changes invalidate accumulation and which Vulkan dependency protects each update.
Create a second storage image
Create an accumulation image with a floating-point format supported for storage-image use, such as VK_FORMAT_R32G32B32A32_SFLOAT when the selected device reports the required format features. Its extent matches the traced output. Give it storage and transfer-destination usage so it can be cleared, allocate compatible device-local memory, create an image view and add a storage-image descriptor binding.
The existing display image remains the encoded result copied to presentation. The accumulation image stays in VK_IMAGE_LAYOUT_GENERAL while the compute shader reads and writes it. Initialise it to zero before the first sample sequence and after every invalidation.
VkDescriptorImageInfo accumulationDescriptor{
.sampler = VK_NULL_HANDLE,
.imageView = accumulationView,
.imageLayout = VK_IMAGE_LAYOUT_GENERAL
};
// A new storage-image binding in the existing set.
VkWriteDescriptorSet accumulationWrite{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.dstSet = descriptorSet,
.dstBinding = 7,
.descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.pImageInfo = &accumulationDescriptor
};
Do not assume every format can be used for every image operation. Query the physical device's format properties and check the features required by the chosen tiling and use.
Make the sample index part of the input
A small integer hash can produce two repeatable values for this teaching renderer. The exact sequence is less important than the contract: pixel coordinates and sample index determine the offset, and repeating the same state repeats the same sample.
uint mixBits(uint value) {
value ^= value >> 16;
value *= 0x7feb352du;
value ^= value >> 15;
value *= 0x846ca68bu;
value ^= value >> 16;
return value;
}
float unitFloat(uint value) {
return float(value & 0x00ffffffu) / 16777216.0;
}
vec2 sampleOffset(uvec2 pixel, uint sampleIndex) {
uint seed = mixBits(pixel.x ^ (pixel.y * 0x9e3779b9u) ^
(sampleIndex * 0x85ebca6bu));
return vec2(unitFloat(seed), unitFloat(mixBits(seed + 1u)));
}
The offset lies in [0, 1) for each component and replaces the fixed centre offset of (0.5, 0.5). A production sampler deserves stronger statistical analysis; this hash is an inspectable source of deterministic variation, not a claim of optimal distribution.
Worked Vulkan account
How Sample History Becomes a Valid Average
Accumulation is an account of compatible sample sums and counts. It remains valid only while the camera, scene and sampling question remain unchanged.
What the code must establish
Each pixel stores a linear sum and the number of compatible samples contributing to it. A new dispatch adds sample colours to the sum and increments the count; display uses their quotient. Camera, geometry, material, light or sampling changes invalidate that history, so a scene revision clears the accumulation state before the next write.
What to inspect
For one pixel, retain the sample offset, linear sample colour, previous sum and count, updated sum and count, and displayed mean. Repeat the sequence from a cleared image and require the same deterministic values. Then change one scene input and establish that the reset occurs before new samples are mixed with the old question.
What this establishes, and what it does not: A smoother image establishes neither an unbiased sampler nor a valid history. The retained sum, count, sample sequence and reset evidence decide whether the displayed average has the meaning claimed for it.
Accumulate a sum with its count
Let sampleIndex count completed prior samples for the same pixel history. If previousSum contains their linear sum and sampleColour is the new linear estimate, update both the sum and its count:
vec4 stored = imageLoad(accumulationImage, pixel);
vec3 previousSum = stored.xyz;
float previousCount = stored.w;
vec3 newSum = previousSum + sampleColour;
float newCount = previousCount + 1.0;
vec3 newMean = newSum / newCount;
imageStore(accumulationImage, pixel, vec4(newSum, newCount));
vec3 displayColour = pow(max(newMean, vec3(0.0)),
vec3(1.0 / 2.2));
imageStore(outputImage, pixel, vec4(displayColour, 1.0));
Storing count in alpha makes each pixel record self-describing during debugging. If all pixels always receive exactly one sample per dispatch, a uniform global count is also valid. Do not combine a global numerator with per-pixel counts or vice versa, and do not mistake the stored sum for the display value.
The sample path returns linear radiance. Display encoding occurs only for outputImage. Encoding each sample before averaging produces a different and biased result.
Update a running mean
A pixel's first three linear red-channel samples are 0.2, 0.8 and 0.5. After two samples the stored sum is 1.0 with count 2, so the displayed mean is 0.5. The third update stores sum 1.5 and count 3, again displaying 0.5. If the stored value 1.0 were treated as an already averaged colour, the history would appear twice as bright. The value and its interpretation are one contract.
State the image dependency between dispatches
A later dispatch reads the accumulation image written by the earlier dispatch. Between them, establish a compute-to-compute memory dependency with storage write as the source access and storage read/write as the destination access. The layout may remain GENERAL; a layout transition is not required merely to make prior writes visible.
VkImageMemoryBarrier2 accumulateAgain{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT |
VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.image = accumulationImage,
.subresourceRange = colourRange
};
If several samples are traced inside one invocation and only one mean is written at the end, no cross-dispatch barrier is needed between those private calculations. The dependency follows actual resource access, not a ritual after every arithmetic operation.
Reset history when its question changes
Camera position, orientation, field of view, image extent, geometry, materials, lights, bounce policy and sampling policy all affect the estimate. Changing any of them invalidates the accumulated mean. Record a clear-to-zero operation for the accumulation image, transition or synchronize it correctly for the clear, and set the sample index back to zero.
A renderer can maintain a monotonically increasing scene revision. The accumulation controller records the revision used for the current history; a mismatch requests one reset before the next dispatch. This is less fragile than remembering to reset in every user-interface callback.
Do not confuse samples with frames in flight
A sample count describes statistical history for a pixel. Frames in flight describe how many submissions and presentation resources the CPU and GPU may be processing concurrently. Increasing one does not automatically increase the other.
If two in-flight submissions both read and update the same accumulation image without ordering, the mean has a data race. The simple course implementation serialises accumulation updates with semaphores and the stated image dependency, or uses one accumulation target per in-flight frame and combines them deliberately. Never rely on apparent queue order across unrelated submissions without the appropriate synchronization.
Move the camera after 64 samples
The camera moves one centimetre. Should sample 65 be averaged into the existing history?
The answer
No. The camera now asks different rays, so the previous 64 samples estimate a different image. Clear the accumulation image and begin the new camera state at sample zero. Retaining the history would produce ghosting, not additional accuracy.
Official reference: storage-image operations and image formats are defined in the Khronos image operations chapter, with memory dependencies defined by the synchronization chapter.
Prove the accumulation arithmetic
For one selected pixel, record the first eight offsets, linear sample colours, stored sums, counts and displayed means. Recalculate every running value independently on the CPU. Repeat the render from a cleared image and establish that the sequence is identical. Then change only the light position and demonstrate that the scene revision clears the history before the next write. Finally, disable the reset deliberately in a debug build and capture the resulting stale mixture so that its cause is recognisable.
Keep this tracer as the reference route
Retain the compute pipeline, explicit primitive loops, counters and accumulation path. Lesson 27 will introduce hardware acceleration structures for triangle traversal, but it will compare their hit distances and identities against this route. Hardware traversal changes how candidates are found; it does not change the camera, interval, material, lighting or accumulation contracts.
Lessons 15 to 26 now form a complete Vulkan compute renderer whose major calculations remain visible in ordinary shader code. It is intentionally expensive, and that expense is useful: it provides a trusted reference against which acceleration can be judged. We are ready to ask the hardware for traversal help without surrendering the meaning of a ray.