libconstruct path tracer
The engine seam, the compute kernel, sampling, culling, the denoisers and the hosts of the path tracer
Status: One engine seam, two engines, four hosts. Built and run on Vulkan (an RTX 5070 and lavapipe); the Direct3D 12 measurements and the Android runs are owed, and Metal is designed for but untested. Last Updated: 2026-10-19 Spec:
rendering-revamp(internal), which absorbed the seams ofconstruct-render-contextand extends the one engine ofconstruct-realistic-rendering
The path tracer is a compute kernel behind a render engine seam. An engine makes a radiance frame and the images of its primary hit, and everything after it (the denoiser, the composite, tone mapping, post and the overlays) is shared with the raster engine. The same engine class runs in the viewport host, the inline viewport's host and the exporter, so an export at N samples and the viewport at N samples agree.
The engine seam
construct::rendering::IRenderEngine, in construct/rendering/IRenderEngine.hpp, is the seam between how a pixel gets its color and the rest of the renderer. One instance lives on one thread and one QRhi for its whole life. The host owns the frame: it begins it, calls advance() with the frame's command buffer, ends it, calls frameEnded(), and runs the frame pipeline on frame().
| Operation | Meaning |
|---|---|
id() |
"raster" or "path-traced" |
setView(viewVersion) |
A new view: camera, lens, size or scene. A progressive engine restarts its accumulation at the next advance(), and each frame it makes after this carries the version |
prepare() |
Builds or reuses the pipelines without blocking. False while a shader or a pipeline still builds; the host then shows its last good frame |
advance(cb) |
Records at most one slice of work into cb. Every submission stays under the device's submit budget. False when nothing was recorded |
frameEnded(cb, wallMs) |
The frame's work is done: the engine reads what the frame read back and learns its cost |
frame() |
The current EngineFrame, or an invalid one |
release(), onDeviceLost() |
Free the device resources, or drop them without touching a lost device |
construct::rendering::EngineFrame, in construct/rendering/EngineFrame.hpp, is what every engine hands on:
| Field | Content |
|---|---|
radiance, radianceGeneration |
Linear, scene-referred radiance, and the generation of the target it lives in. A consumer that caches bindings keys on the generation, never on the pointer |
gbuffer |
A GBufferBinding: depth, item id, normal and material word of the primary hit, with their generation |
albedo |
The first non-specular albedo (RGBA8), for a denoiser |
meta.viewVersion |
The view the frame was made for. The staleness gate compares it |
meta.samplesDone, meta.sampleTarget |
Samples accumulated and the target; 1 and 1 for a raster frame |
meta.noise, meta.converged |
The noise level of a progressive engine, and whether it has nothing more to add |
meta.progressive |
True for a path-traced frame. Only these frames go through the denoiser |
meta.gpuMs |
The GPU time of the work that made the frame, 0 when unknown |
construct::rendering::EngineDescriptor, in EngineDescriptor.hpp, says whether an engine is progressive. A host asks engineForShadingMode(mode).progressive, never the shading mode: IShadingMode::engineId() names the engine that a mode is a tier of. The pbr and env-lit-pbr modes are tiers of the raster engine, and path-traced is the path-traced engine.
The engines:
| Class | Header | What it is |
|---|---|---|
construct::rendering::RasterEngine |
construct/rendering/RasterEngine.hpp |
A non-owning engine over RenderPipeline. advance() is RenderPipeline::renderScenePass(), one frame per call. The host names the target first with setTarget() |
construct::pathtrace::PathTracedEngine |
construct/pathtrace/PathTracedEngine.hpp |
The path tracer behind the seam. advance() records the tiles of the current sample that fit the slice the host was granted (setSliceMs()), cut by the SubmitPlanner. frame() is the last complete sample, with the view version that sample was traced for |
construct::pathtrace::PathTraceEngine |
construct/pathtrace/PathTraceEngine.hpp |
The accumulation core inside PathTracedEngine: the kernel pipeline, its buffers and images, stepTile(), primeKernel(), the noise readback and the environment bake. No thread and no timer of its own |
PathTracedEngine also reports what a host and a test need: completedSample(), converged(), sampleIndex(), lastSubmitMs() (the GPU time where the device has timestamps, the wall time otherwise), maxSubmitMs(), submissionCount(), restartCount(), recentSubmissions() (the last 24), planDescription() and lastError(). setMaxStepsPerSubmit() carries the exporter's Steps per submit cap, from 1 to 64.
The parts
| Class | Header | Role |
|---|---|---|
construct::pathtrace::PathTraceSceneSnapshot |
PathTraceSceneSnapshot.hpp |
What the engine renders: the nodes, the material module's instance rows and parameters, the emitters, the culling tree, the definition data, the camera and the thin lens, the environment and lights, the march uniforms, the budget, and the kernel's SPIR-V (sdfTracerKernelSpirv) |
construct::pathtrace::PathTraceSnapshotBuilder |
PathTraceSnapshotBuilder.hpp |
build() from the scene, the camera, the SDFLayer, the environment, the lighting, the size and the budget |
construct::pathtrace::SampleBudget |
SampleBudget.hpp |
Every limit of the tracer, with one set of defaults. forViewport() gives the viewport's budget, sanitized() replaces each value out of range with its default |
construct::pathtrace::SubmitPlanner |
SubmitPlanner.hpp |
Cuts a sample into tiles so that each submission fits the slice. It measures the cost per pixel, keeps its plan across frames, and setBudgetMs() lets the plan follow the pacer's slice |
construct::pathtrace::SobolBurleySequence, IndependentSequence |
SampleSequence.hpp |
The sample sequences behind ISampleSequence |
construct::pathtrace::CyclesAdaptiveCriterion |
AdaptiveCriterion.hpp |
The C++ twin of the kernel's per-pixel stop, and the frame stop, behind IAdaptiveCriterion |
construct::pathtrace::buildEmitterList() |
EmitterList.hpp |
The emitter list at binding 12 |
construct::pathtrace::SceneClusters, SceneBvh |
SceneClusters.hpp, SceneBvh.hpp, SceneClusterBounds.hpp |
The clusters of the scene, their boxes, and the tree at binding 14 |
construct::pathtrace::PathTraceProducer, PathTraceClient |
PathTraceProducer.hpp, PathTraceClient.hpp |
The host of the inline viewport: a rhi::WorkerHost producer and its main-thread client |
construct::rendering::PresentSchedule |
construct/rendering/PresentSchedule.hpp |
When a still view presents |
construct::rendering::CyclesResolutionController |
construct/rendering/ResolutionController.hpp |
The navigation divider, behind IResolutionController |
construct::rendering::OpenPbrTables |
construct/rendering/OpenPbrTables.hpp |
The OpenPBR BSDF's GLSL and its tables at binding 17 |
construct::shader::MaterialModule |
construct/shader/MaterialModule.hpp |
The material module and its buffers at bindings 15 and 16 |
construct::denoise::IDenoiser and its two implementations |
construct/denoise/ |
Stage 2 of the frame pipeline |
The uniform block is construct/layers/SDFUniforms.hpp, 976 bytes, a static_assert. testGLSLValidation checks every field's std140 offset against the kernel's block. The engine writes one copy of it per tile into its own slot of a dynamic uniform buffer, 64 slots, so a host can record many tiles into one command buffer.
The kernel
The kernel is a compute shader. PathTracedShadingMode::tracerKernelSource() and SDFShaderGenerator::generateTracerKernel() write its GLSL from the scene, and SDFLayer::prepareTracerKernel() builds it through the shader forge whenever the tracer's source changes. The snapshot carries its SPIR-V, and a host waits for it. The forge runs SPIRV-Tools' dead-branch, dead-code and dead-function passes on a compute stage before it makes the other backends' sources, so a lobe that no material in the scene uses does not reach the driver.
A workgroup is 8 × 8 threads, one thread for each pixel of a tile. A thread:
- Takes its position inside the pixel and its lens point from group 0 of the pixel's sample sequence. A thin lens (aperture radius, focus distance, a polygon aperture by the blade count) moves the ray origin, which is why the kernel marches its own primary ray.
- Marches the primary ray, and steps the hit onto the surface as the raster geometry pass does.
- Traces the path from the hit: the shadow rays, the emitter ray and the bounces run as stages of one loop, each step through the one call of
castRay(), so the driver inlines the scene tree at as few sites as possible. - Adds the sample to the accumulation in place, and writes the frame's images.
The bindings of the kernel:
| Binding | Kind | Content |
|---|---|---|
| 0 | uniform buffer, dynamic offset | SDFUniforms: the march, the camera, the lens, the budget, tracerTile, tracerFlags |
| 1 | storage buffer, read | The SDF nodes |
| 2 | storage image, write-only, RGBA32F | The radiance of the sample: the mean, with the depth in alpha |
| 3 | storage image, write-only, RGBA16F | The world-space normal of the primary hit |
| 4 | storage image, write-only, RGBA8 | The first albedo of the primary hit |
| 5 | storage buffer, read | The accumulation that this sample reads |
| 6 | storage image, write-only, R32F | The window depth of the primary hit, 1 for a miss |
| 10 | storage buffer, read | The definition data table, declared when a definition with data is in use |
| 11 | storage buffer, read | The environment's sampling table |
| 12 | storage buffer, read | The emitter list |
| 13 | storage buffer, write | The accumulation that this sample writes |
| 14 | storage buffer, read | The clusters and their tree |
| 15 | storage buffer, read | The material module's instance rows |
| 16 | storage buffer, read | The material module's parameters |
| 17 | storage buffer, read | The OpenPBR tables |
The accumulation is two storage buffers that take turns: binding 5 reads one and binding 13 writes the other. Per pixel each holds two vec4: the mean of the samples with the depth, then the mean of half the samples with the pixel's error. After them, the buffer that a bake writes holds the environment's grid of 256 × 128 texels. The images take turns with the buffers, so the radiance, the depth, the normal and the albedo of frame() always belong to the same complete sample. A read-write RGBA image is not portable, which is why the values that the kernel reads back live in buffers and the images are write-only.
A sample is a list of tiles. A host calls stepTile() for each, and the last one swaps the buffers and counts the sample, so the image and the noise readback show whole samples only. primeKernel() dispatches once over an empty tile before the planner's first measurement, for a device that compiles a pipeline at its first dispatch.
Sampling
| Part | What the kernel does |
|---|---|
| Sequence | SobolBurleySequence: Owen-scrambled Sobol by hashing (Burley 2020). Each pixel scrambles one Sobol sequence with a seed of its own, and each group of four dimensions with another. The first sample of each pixel is the Sobol point at the pixel's shuffled Morton index, with one seed for all pixels, so a frame of one sample is evenly spread. The GLSL is ss_sample4(sampleIndex, pixel, group), and sample4() is the same function in C++ |
| Dimensions | Group 0: the position in the pixel and the lens point. Group 1: time and wavelength, reserved and unread. Bounce b: groups 2 + 2b and 3 + 2b. After bounce 3 the values come from the PCG hash |
| Integrator | Unidirectional path tracing with next-event estimation and multiple importance sampling, power heuristic with β = 2 |
| Environment | The engine bakes the environment's radiance into an equal-area grid of 256 × 128 cells, reads it back in the same frame, builds a luminance CDF with a floor of 1 % of the mean (buildEnvironmentSampling()), and binds it at 11. The cosine sampler serves until the table lands, and the table lands only where a sample starts. Environment next-event estimation (ptEnvironmentNee) turns it off |
| Emitters | buildEmitterList(): an item whose material instance emits is an emitter. Its bounding sphere is centered at the node's world origin, with a radius of 1.75 times its largest length parameter times its largest scale. The kernel picks an emitter by its power times its solid angle, samples a direction in the cone of its sphere, and weighs it with MIS against the BSDF sample. At most 64, the most powerful first |
| Soft sun | A directional light's direction.w is its angular radius, sampled as a cone. The physical sky's sun is 0.004651 rad, 0.53 degrees across |
| Russian roulette | From the second bounce, with a survival of the throughput's largest channel, at most 0.95 |
| Bounce limits | Separate limits for diffuse, glossy and transmission bounces: ptMaxBounces 4, ptMaxGlossyBounces 4, ptMaxTransmissionBounces 12, each at most 21. One path holds at most 64 vertices |
| Firefly clamp | The luminance of the indirect light of one sample is clamped to ptFireflyClamp, 100 by default, 0 off. Direct light is never clamped |
| Ray cones | Each ray carries a cone. A secondary ray's hit epsilon is half its width (at least ptSecondaryEpsilon), the normal step is the width, and a ray leaves a vertex at 2 × max(epsilon, |f(p)|). The material module reads the width as its filter footprint |
| Nested dielectrics | A stack of at most four media per path, each with its material's priority. A higher priority wins, and a surface inside a medium that outranks it is passed |
| Media | Beer–Lambert in an absorbing medium. A random walk in a scattering one (Chiang, Kutz and Burley 2016), at most 256 events |
Adaptive sampling. The kernel writes each pixel's error into the half buffer: Σ|I − A| / (1e-4 + √ΣI), with I the mean of all samples and A the mean of half of them (Cycles' criterion). A pixel stops when its error and its eight neighbors' errors are under the threshold. Every 32 samples the host reads the half buffer back (recordNoiseReadback()), and PathTraceEngine::reportNoise() stops the frame when 99.9 % of its pixels are under the threshold, after the minimum samples. The 95th percentile is the noise level that the badge and EngineFrame::meta.noise show. A threshold of 0 turns adaptive sampling off. A converged engine issues no GPU work until the view or the scene changes.
The material module and the BSDF
The material module, construct::shader::MaterialModule, is the GLSL of every material definition in use: one mat_<n>(paramBase) per definition, mat_inline (definition 0) for an item that holds an explicit material, and evalMaterial(instance, ctx), which dispatches on the instance row. The instance rows are at binding 15 and the parameters at 16. A definition's every number is a parameter slot, so a value edit is an upload and never a compile. The module's source changes only with the set of definitions. While a new module compiles, an instance of a definition that the old shader lacks renders its own flat values through mat_inline.
The raster passes call the same module, so a material looks the same in both engines, up to the light transport.
The BSDF is OpenPBR Surface 1.1.1, ported from adobe/OpenPBR-Implementation (Apache-2.0) as construct/shaders/OpenPbrBsdf.glsl. Its seven energy tables and its LTC table, 72 736 values, are a storage buffer at binding 17, uploaded once, and the GLSL reads them through openpbr_table(). OpenPbrTables::bsdfGlsl() and bufferAccessorGlsl() give the code. The kernel prepares the BSDF once per vertex, and calls evalBsdf(), sampleBsdf(), prepareBsdf() and materialAtHit() from one site each. A lobe group that no definition in the module uses is switched off (OPENPBR_Enable*) and pruned before the driver sees it. A material extension, such as the car-paint flakes, links its code into the BSDF when a definition sets one of its channels.
Culling
| Part | What it is |
|---|---|
findClusters() (SceneClusters.hpp) |
A cluster is a subtree that must be evaluated as a whole. Every root is a cluster, and so is every operand of a hard union reached through hard unions only. A union under a rigid transform splits, and its operands start in the transform's frame. Anything else keeps its subtree in one cluster |
Bounds (SceneClusterBounds.hpp) |
A leaf's box from its field, grown by its density and shell, carried through the affine transforms above it. A subtraction keeps its first operand's box, and a smooth operator grows by its blend radius. A modifier, a transform that is not affine or a leaf with no box leaves its cluster unbounded |
SceneBvh |
A binned SAH tree over the cluster boxes: 12 bins, leaves of at most 4 clusters, a depth of at most 30. refit() moves the boxes without a new tree when only boxes move. packed() is the storage buffer at binding 14, and rootsOnly() stands in when the snapshot has no tree |
The kernel's sceneSDF walks the tree at each point it evaluates: the unbounded clusters first, then the tree, nearer child first. It skips a box that the point is outside of and that is no nearer than the smallest distance found so far, and it never skips a box that the point is inside. So the march's step stays safe, and the normal, the shadow test and the bounce origin use the same query as the march. On the 113-sphere bench scene at 256 × 144 and 4 samples, the tree took the trace from 4 198 ms to 1 084 ms.
The denoisers
construct::denoise::IDenoiser, in construct/denoise/IDenoiser.hpp, takes a DenoiseInputs (the radiance, the albedo, the normal, the depth, their size, the samples, and a DenoiseView that rebuilds world positions from the depth) and returns a denoised radiance texture. It returns null when it cannot run, and the host then shows the radiance it has. makeDenoiser("atrous") and makeDenoiser("oidn") make the two implementations.
| Class | What it is |
|---|---|
construct::denoise::AtrousDenoiser |
The edge-avoiding à-trous wavelet filter (Dammertz and others, 2010): 5 fullscreen fragment passes of a 5 × 5 B3-spline kernel into RGBA16F targets, with the step 2^i in pass i. The stops are on the color (relative to the center's luminance, halved with each pass), the normal, and the distance from the center's tangent plane. It divides the radiance by the albedo first and multiplies it back last. A miss passes through. The shader is shaders/post/atrous.frag, and atrousReference() is the same filter on the CPU |
construct::denoise::OidnDenoiser |
The Open Image Denoise rt network (a U-Net of 3 × 3 convolutions) in the project's own compute shaders, shaders/denoise/oidn_*.comp, with the published weights rt_hdr_alb_nrm.tza (Apache-2.0) in construct/denoise/oidn/. OidnWeights reads the TZA file, OidnNetwork holds the layer list and oidnReference(), the CPU network. Tensors are 16-bit floats, and weights and sums are 32-bit. Tiles are at most 1024 pixels on a side by default (setMaxTileSide()), with an overlap of 96 pixels, and at least 384. The input scale is automatic, or fixed with setInputScale() |
OidnDenoiser::denoise() records the whole network into one command buffer. A host that keeps each submission under the submit budget calls begin(), then recordSteps() over several submissions, and reads output(). Each step knows its work in multiply-adds (stepWork()), so a host plans its submissions by a measured time per unit of work.
The policies:
| Host | Setting | What runs |
|---|---|---|
| Viewport | ptViewportDenoiser: 0 off, 1 the à-trous filter (default), 2 the à-trous filter, then OIDN |
The à-trous filter on each frame it shows (ConstructViewportProducer::denoiseTracedFrame()). With 2, the network also runs when NetworkSchedule::due() says so: at 64 samples, at convergence or at a pause, then every 2 s while the samples grow. While it runs, the tracer traces nothing, its steps are paced slices of the progressive class, and the badge says "Denoising". Its frame shows until the view changes or a newer run ends |
| Export | ptDenoiser: 0 off (default), 1 the à-trous filter, 2 OIDN |
Once, at the end of each path-traced frame (ConstructSceneRenderer::denoiseTracedFrame()). The network's steps go in submissions of at most half the submit budget; a step over the budget halves the tile side for the next frame, down to 384 |
construct render --denoise off|atrous|oidn, the denoise input of view.snapshot |
As the export | As the export |
The inline viewport's host has no denoiser.
The hosts
| Host | Thread and device | What it adds |
|---|---|---|
Viewport (ConstructViewportProducer) |
The viewport's rhi::WorkerHost thread and its QRhi |
Runs PathTracedEngine on the viewport's own device, so no second device and no cross-device copy exist. Each tick it runs paced slices of the progressive class for at most 20 ms, and ends early when a sample completes. It denoises the sample, and SDFLayer::setPathTraceCompositeTexture() composites it in the scene pass, with its own depth image. It drives the navigation divider (CyclesResolutionController), the present schedule, the raster fallback (SDFLayer::setNavigationTierActive()) and pause. The tracer stays warm for 30 s after the viewport stops to path trace |
Inline viewport (PathTraceProducer, PathTraceClient) |
A WorkerHost of its own, with its own device. The GUI thread never runs the tracer |
One paced slice of the progressive class per tick, a readback of the radiance (RGBA32F) into PathTraceResult, which SDFLayer::setPathTraceClient() uploads and composites |
| Material preview, Rendered | A SceneWorkerWidget of its own, so a ConstructViewportProducer on a worker thread and a device of its own |
As the viewport, at the preview's size |
Exporter (ConstructSceneRenderer, run by ConstructRenderWorker) |
The job thread of the worker, one thread that never expires, and an offscreen QRhi |
Runs the engine to the final budget, asks the pacer for a batch slice before each submission, checks the time budget after each submission once a sample is complete, denoises the last complete sample and composites it with setPathTraceDecoupledHost(). The renderer, its device and its plan live across frames and jobs, and the device goes 30 s after the last job. construct render and OPAL stills run the same worker |
The GPU pacer and the present schedule
rhi::GpuPacer, in librhi/rhi/GpuPacer.hpp, is one per process. A host calls requestSlice(workClass) before a submission and gets a GpuSliceGrant: a slice in milliseconds, or a wait. After the submission it calls reportSlice(workClass, busyMs). The policy is IGpuPacingPolicy, and the default is DutyCapPacingPolicy (id duty-cap):
GpuWorkClass |
Slice | Cap |
|---|---|---|
Interactive |
One frame at 60 fps | Never waits, never counts |
Progressive |
progressiveSliceMs while the GUI is active, 4 times as long when it is idle |
Always under the duty cap |
Batch |
The submit budget (RenderBudget::submitBudgetMs, 100 ms by default) |
Under the duty cap while the GUI is active. None while it is idle or while setBatchExclusive() holds (pauseViewportDuringRender) |
The duty cap is gpuDutyCap (0.8) of a window of 1000 ms. A grant that would pass it waits until enough older slices leave the window. watchInput() installs an event filter that marks the GUI active for 500 ms after each mouse, key, wheel, touch or tablet event. ConstructShell::applyGpuPacing() hands the two settings to the pacer, and the thumbnail worker, the viewport host, the inline host and the exporter all ask it.
construct::rendering::PresentSchedule decides when the viewport host presents a still view: every 100 ms in the first second, every 250 ms until 2 s, 500 ms until 4 s, 1 s until 8 s or 32 samples, then every 2 s. A view change restarts it, and the first sample of a view and the final frame always present.
Threading
GUI thread Viewport worker thread (rhi::WorkerHost, the viewport's QRhi)
========== ===============================================================
ConstructShell ConstructViewportProducer
├─ scene, camera, settings ──────────► ├─ PathTraceSnapshotBuilder::build, setView(version)
├─ rhi::GpuPacer::watchInput ├─ tick: advanceTracer
│ │ ├─ GpuPacer::requestSlice(Progressive)
│ │ ├─ PathTracedEngine::advance (tiles, own submission)
│ │ └─ frameEnded, reportSlice
│ ├─ denoiseTracedFrame (stage 2)
│ ├─ SDFLayer composite (stages 3, 4)
│ ├─ RasterEngine::advance + runFramePipeline
SceneWorkerWidget ◄──── WorkerFrame ─────┴─ viewVersion, samplesDone, sampleTarget, converged, divider
└─ tracerProgress → the badge
Inline viewport: PathTraceClient (GUI thread) ─► WorkerHost thread, own QRhi: PathTraceProducer ─► PathTracedEngine
Exporter: ConstructRenderWorker job thread, offscreen QRhi: ConstructSceneRenderer ─► PathTracedEngine
Lifecycle and errors
The viewport host makes its tracer when the viewport starts to path trace, binds it to the viewport's device, and builds its resources lazily at the first prepare() that has a kernel. prepare() returns false while the kernel still builds, and the viewport shows its last good frame. A create that fails is reported once, as "Path tracer: " and the reason, in the viewport's status. These are the reasons that a device can give:
- "this backend has no compute or no storage buffers (needs ES 3.1+ / Compute) — cannot path-trace"
- "this device cannot store to an RGBA32F, RGBA16F, R32F or RGBA8 image — cannot path-trace"
- "target WxH exceeds the accumulation pixel budget", above the render budget's
maxAccumPixels
The inline host reports the same reasons through WorkerFrame::error and PathTraceClient::workerError(). A lost device calls onDeviceLost(), which drops every resource without touching it. The host rebinds the engine to its new device, and the accumulation starts again. Leaving a mode that path traces keeps the viewport's tracer for 30 s, so a quick return compiles nothing again.
Settings
The settings of the Path tracing category reach the tracer through SampleBudget. Their defaults come from it, and the OPAL settings tasks set them by id. A change restarts the accumulation.
| Id | Label | Default | Range | Levels |
|---|---|---|---|---|
ptMaxSamples |
Max samples | 1024 | 1 to 65536 | All |
ptMinSamples |
Min samples | 16 | 2 to 4096 | All |
ptNoiseThreshold |
Noise threshold (0 = fixed count) | 0.01 | 0 to 1 | All |
ptTimeBudgetMs |
Time budget (ms) | 10000 | 0 to 600000 | All |
ptDenoiser |
Final denoiser | 0, off | 0, 1, 2 | All |
ptMaxBounces |
Max diffuse bounces | 4 | 0 to 21 | All |
ptMaxGlossyBounces |
Max glossy bounces | 4 | 0 to 21 | All |
ptMaxTransmissionBounces |
Max transmission bounces | 12 | 0 to 21 | All |
ptFireflyClamp |
Firefly clamp | 100 | 0 to 10000 | All |
ptEnvironmentNee |
Environment next-event estimation | on | on, off | All |
ptSecondarySteps |
Secondary steps | 64 | 1 to 1024 | All |
ptSecondaryEpsilon |
Secondary epsilon | 0.002 | 0.00001 to 0.1 | All |
ptRenderScale |
Render scale | 1.0 | 0.05 to 1.0 | All |
ptStepsPerSubmit |
Steps per submit | 16 | 1 to 64 | All |
ptViewportNoiseThreshold |
Viewport noise threshold | 0.1 | 0 to 1 | App, scene, camera |
ptViewportMaxSamples |
Viewport max samples | 1024, 0 none | 0 to 65536 | App, scene, camera |
ptViewportDenoiser |
Viewport denoiser | 1, à-trous | 0, 1, 2 | App, scene, camera |
ptViewportPixelSize |
Viewport pixel size | 0, automatic | 0, 1, 2, 4, 8 | App, scene, camera |
ptNavigationFallback |
While the view moves | 0, automatic | 0 automatic, 1 Material shading, 2 coarse trace | App, scene, camera |
"All" is the app, the scene, a camera and a render job. The viewport takes at least 4 samples before its threshold applies. Two machine-local settings of the This machine category feed the pacer: gpuDutyCap (GPU share for background rendering, 0.8, 0.1 to 1.0) and progressiveSliceMs (Background GPU slice (ms), 8, 1 to 50). The presets Draft, Standard and Final set ptMaxSamples, ptMinSamples, ptNoiseThreshold, ptMaxBounces, ptMaxGlossyBounces and ptDenoiser.
Backends
The kernel needs QRhi::Compute, storage buffers, and write-only storage images of the formats RGBA32F, RGBA16F, R32F and RGBA8. It reaches the GPU through QRhi only, with no backend-specific code: Vulkan, Direct3D 12 (through DXC), OpenGL ES 3.1 and up, and desktop OpenGL with compute. Metal has compute, and the design covers it, but no Apple build exists to test it. tracerFlags.x tells the kernel that the first row of a target is the bottom of NDC, on Direct3D and Metal, and a denoiser takes the same fact from DenoiseView::bottomUp. rhi::BackendCaps::timestamps tells the engine whether the device measures GPU time; without it, the engine reports the wall time to the pacer.
Related
- Construct rendering: the frame pipeline after an engine.
- Construct rendering modes: the user's view of path tracing in the viewport, navigation, refinement and the badge.
- Path-traced quality: the settings in practice.
- Understand viewport transport and performance: the GPU share, the pixel size and the benches.
- Internal:
rendering-revamp: the architecture, the path tracer, the viewport UX, the performance plan and the phase records.