Understand viewport transport and performance
Which frame transport your platform uses, what zero-copy means, how the GPU is shared with the path tracer, how large a solve can be, and what to attach to a bug report
The Construct viewport renders on a worker thread with its own GPU context, then hands each finished frame to the editor window. The hand-off is called the transport. The editor picks the transport automatically per platform. There is nothing to configure.
Which transport your platform uses
| Platform | Graphics API | Transport |
|---|---|---|
| Windows 11 | Direct3D 12 | Zero-copy shared texture |
| Linux | Vulkan or OpenGL | Readback |
| Android | OpenGL ES | Readback |
Zero-copy means the worker renders straight into a GPU texture that the editor window samples directly. The frame never crosses the CPU, so the hand-off costs close to nothing per frame. On Windows 11 this is the default and needs no setup.
Readback means the finished frame is copied from the GPU to memory once, then drawn into the window. This is the portable path, and it is also the automatic fallback everywhere: if zero-copy is unavailable â a second GPU, a driver that refuses the shared texture â the viewport falls back to readback and keeps working. Nothing goes black and nothing needs restarting.
The application log always states which transport runs. Search the log
for lines starting with transport: â you will find either
transport: ZERO-COPY leg ACTIVE or a line naming the readback leg and
the reason.
Responsiveness and scene size
Editor responsiveness does not scale with the size of the scene. One edit costs one bounded unit of work: the editor re-evaluates only the values that depend on what you changed, and it renders one frame for it. A change to one parameter in a scene of a thousand instances costs the same as the same change in a scene of ten.
Earlier releases did not behave this way. Every single-parameter change re-evaluated every expression in the document, so a model got slower to edit as it grew. If you were told that "it gets sluggish on big scenes" is expected, that advice is out of date. A scene that is slow to edit today is a problem to report, with the log and the FPS counter readings below.
Two things still grow with the scene, and both are render costs rather than edit costs: the first frame after a structural change (adding or removing items rebuilds the GPU scene once), and the raymarch itself at high resolution.
Large tet volumes
A tet volume's cost grows with its number of tetrahedra, and the bake resolution decides that number. Each doubling of the cell count makes about eight times as many tetrahedra. The "Auto" resolution, 256 cells across the largest extent, gives about 22 million tetrahedra on a model of a sphere and a box joined together.
What happens after a tet bake:
- The bake runs as a job. The editor stays usable.
- The finished volume goes to the scene without a read of its file.
- The outline of the volume (its boundary) is computed in the background. The volume shows when that is done. At the "Auto" resolution this can take half a minute.
Until step 3 is done, the volume does not show, but the camera, the gizmos and every other item respond. A deleted volume leaves the view at once. Opening a scene that holds a tet volume reads the volume's file in the background in the same way.
If a volume is larger than you need, bake it again at a lower resolution in the bake dialog. Half the cells gives an eighth of the tetrahedra.
Solve sizes
A solve of a tet volume (Tet ⸠SolveâŚ) has three unknowns for each node. Its time and memory grow with the unknowns, and the solver decides how fast.
| Solver | Time | Memory | Use it for |
|---|---|---|---|
| Direct | Grows as about the square of the unknowns, or faster | Grows faster than the unknowns | Small volumes. It gives an exact answer and cannot fail to converge |
| Iterative | Grows a little faster than the unknowns | Grows with the unknowns | Every volume above about 15 000 unknowns |
Auto picks Direct up to 15 000 unknowns and Iterative above. The Solve dialog shows the unknowns and the memory of each choice before you solve.
Measured on a 64-thread Linux workstation with 125 GiB, on a sphere and a box joined together, in steel:
| Tetrahedra | Element | Unknowns | Solver | Time | Peak memory |
|---|---|---|---|---|---|
| 10 150 | Tet4 | 7 770 | Direct | 0.2 s | 16 MiB |
| 10 150 | Tet4 | 7 770 | Iterative | 0.1 s | 6 MiB |
| 88 510 | Tet4 | 57 567 | Direct | 33 s | 397 MiB |
| 88 510 | Tet4 | 57 567 | Iterative | 1.4 s | 82 MiB |
| 1 060 926 | Tet4 | 655 977 | Iterative | 33 s | 1.0 GiB |
| 2 067 325 | Tet4 | 1 232 526 | Iterative | 1.5 min | 1.9 GiB |
| 88 510 | Tet10 | 384 216 | Iterative | 52 s | 1.2 GiB |
| 495 908 | Tet10 | 2 141 295 | Iterative | 14 min | 6.6 GiB |
The times include the assembly and the stress. Other work on the machine gave a load average of 4 to 13 while they ran. A quadratic volume (Tet ⸠Quadratic elements) has about six and a half times the unknowns of the same linear one, and each unknown costs about twice the memory. It is more accurate in bending.
The solve runs on one thread. A solve that needs more memory than any machine of the pool has is refused before it starts, with the reason.
Field values, probes and labels at scale
The value under the pointer, the probes and the labels of a solved volume read the field through a locator of its tetrahedra. The locator builds once for each volume, on a background thread. The first hover after a solve says "LocatingâŚ" until it is ready.
These figures are from a release build on a 64-thread Linux workstation with an RTX 5070, at a load average of 16 to 64 from other work. The volume is a ball of 989 808 tetrahedra and 176 093 nodes:
| Measure | Time |
|---|---|
| The locator, after the boundary of the volume is built | 129 ms |
| Locator memory | 13.5 bytes a tetrahedron |
| A hover step, near the last hit | 0.08 Âľs |
| A hover far from the last hit | 9 Âľs, 46 Âľs at the 99th percentile |
| A hover on the deformed volume after a change of the scale | 12 ms |
| 256 probe rows, ten fields | 9.6 ms |
| A CSV of one million probes, ten fields | 0.9 s |
| A line probe of 10 000 samples | 8 ms |
| A new field, its colors on the surface | 111 ms |
| A threshold, after a change of its value | 86 ms to 118 ms |
| Six cut planes with their caps | 59 ms, then 5.5 ms to move one plane |
The probe table computes 256 rows at a time, only for the rows that show, so a table of one million probes scrolls without a wait for the rest. A CSV of that size writes in the background, with a progress row, on every core.
The labels lay out 1 000 anchors in 0.6 ms and paint the 64 boxes that show and the 1 000 marks in 1.8 ms, into a 2560 Ă 1440 view. Past 64 boxes, an anchor draws its mark only.
A kernel over one million elements takes 6 ms on eight threads for the von Mises stress and 44 ms for the principal stresses. The range of a field, its peaks and its extremes take 30 ms or less each.
Every presenter of a volume adds about 1 % to a frame of the handwheel scene. With no other item in the view, the same presenters add about a third to a frame of 1.8 ms.
While a deformation animates, the SDF items keep the image of their last ray march, and each tick of the animation copies it in under the moving volume. On the handwheel scene with a volume of one million tetrahedra, a tick takes 0.48 ms of GPU time at 1280 Ă 720 and 1.4 ms at 2560 Ă 1440, about as much as a frame where nothing moves. A frame that moves the camera marches again: 71 ms and 264 ms. The animation stops while its window is in the background.
Developers measure these figures with construct-bench field-sample,
hover-deformed, labels, field-kernels, field-stream,
threshold-rebuild, cut-caps and presenter-frame-cost (with
--animate for an animation). Each verb prints its counts with its times.
Path-traced rendering and the GPU
The path tracer of the viewport, a render job and the window all share one GPU. The GPU runs its work in the order it gets it, with no priority, so a long piece of tracer work makes the next frame of the window wait. One pacer per process stands in front of the GPU and decides how long each piece of background work can be and when it can start. Each host asks the pacer before it submits its work, and reports what the work cost after it: the GPU time where the device measures it, and the wall time otherwise.
The pacer sorts work into three classes:
| Class | What it is | What the pacer allows |
|---|---|---|
| Interactive | The frames of the window, and the viewport frames of the raster engines while you move the camera, a gizmo or a slider | Always goes at once. It never waits, and it does not count against the GPU share |
| Progressive | The refinement of the path-traced viewport, its navigation frames included, the thumbnails and the material previews | Slices of Background GPU slice (ms) while you work, and four times as long after 500 ms without input. Always under the GPU share |
| Batch | Render jobs, construct render and the stills that OPAL asks for |
Slices of the GPU submit budget, 100 ms by default. Under the GPU share while you work. No limit while you are idle, or when Pause the viewport while a render job runs is on |
"While you work" means up to 500 ms after the last mouse, key, wheel, touch or tablet event in the application.
The GPU share
GPU share for background rendering, in the This machine category of the settings page (Ctrl+,), is the duty cap: the share of each second that progressive and batch work together can keep the GPU busy. The default is 0.8, and the range is 0.1 to 1.0. At 0.8 the background work gets at most 800 ms of GPU time in any one second, and the window and the desktop compositor always have the other 200 ms. When the next slice would pass the cap, the host waits until enough of the older slices leave the one-second window.
Background GPU slice (ms), in the same category, is the longest slice of progressive work while you work: 8 ms by default, from 1 ms to 50 ms. A shorter slice makes input smoother, because no frame of the window waits long behind a slice. It also makes refinement a little slower, because each slice has a fixed cost.
Both settings take effect at once and are stored on this machine only, because the right values depend on the GPU and its driver. Two viewports that path trace ask the same pacer, so they share the cap rather than double it.
In the viewport, the tracer runs its slices for at most 20 ms of each viewport tick, and ends the tick early when a sample is complete, so that the sample can show. A still view shows a new image every 100 ms in the first second, then less often, up to every 2 s. When the view is done, the tracer issues no GPU work at all.
The pixel size
Viewport pixel size, in the Path tracing category, is the size of one traced pixel on the screen: 1Ă, 2Ă, 4Ă, 8Ă or Automatic, the default. Automatic follows the pixel ratio of the display, rounded down to 1, 2, 4 or 8, so a display at a ratio of 2 traces one pixel for each block of 2 Ă 2 screen pixels. Each step from 1Ă to 2Ă to 4Ă traces a quarter of the pixels of the step before, and so costs about a quarter of the time per sample.
While the view moves, a frame starts at eight times the pixel size, and its long side never gets shorter than 128 pixels. The frame time is 1/60 s. A frame that costs more than 1.4 times the frame time doubles the divider, and the divider never gets finer while the view moves. One frame time after the last change, the view traces at the pixel size again. While the view moves decides what shows when even the coarsest frame costs more than twice the frame time: Automatic shows the Environment-lit PBR engine then, Material shading always shows it while the view moves, and Coarse trace never does.
See Troubleshoot rendering problems for the steps to take when the viewport stutters.
Reading the FPS counter
The viewport's FPS counter shows two rates: the render rate (frames that carried new pixels) and the delivered rate. The interface reference explains what each number counts and why they can differ.
Numbers for a bug report
If you report a performance problem, attach the application log â the
transport: line and the FPS counter readings carry most of what a
developer needs. If the viewport stops following a drag, start the
application with --tick-trace and reproduce it: the flag writes a timeline
of render ticks that shows whether a frame was never requested, never
served, or served late. See
Collect useful logs.
Developers can additionally run the frame bench, which records
per-frame timings into a SQLite database (construct-bench frames --frames <n> --db <path>). The bench is a developer tool; its documentation lives with the
internal test harness docs, not here.
Numbers for a slow path-traced render
If a path-traced export is slow, or path tracing in the viewport makes the
window lag, run the bench that measures it and attach its output to the report,
with the application log. construct-bench is built with the other tools.
After kiln build it is debug/construct-bench/construct-bench in the
build folder. Each bench prints its numbers to the terminal, one line per
measurement.
| Bench | What it measures | Needs |
|---|---|---|
pathtrace-export |
Path-traced frames through the exporter, the code that a render job runs. For each frame: the wall time, the prime time, the accumulation time, the samples, the samples per second, the submissions, the longest submission and why the frame stopped | A GPU |
pathtrace-viewport |
A viewport window that path traces. The gaps of the window's event loop (p50, p99 and the largest), the time of each present, the traced frames and samples per second, and the time from a camera move to the next frame and to the first sample of the new view | A GPU and a display. It does not run with -platform offscreen |
pathtrace-stages |
The GPU time of one sample at 0, 1, 2 and 4 bounces, with the light sampling of the sky off and on, and the cost of the first hit, a bounce and the shadow rays, from the differences. A last row traces 4 bounces with the materials and the surface response computed 16 times: the difference over 15 is their cost | A GPU |
material-cost |
The scene's spheres bound to 1, 10 and 100 materials, flat and then procedural. For each count: a Material-mode frame and a one-bounce sample | A GPU |
denoise-cost |
The Ă -trous filter, one of its passes, and the OIDN network on a noisy frame that the bench makes. For the network: its tiles, its longest submission and its multiply-adds per second | A GPU |
Run the benches on your own scene where they take one, and attach every line that they print:
construct-bench pathtrace-export --scene my-scene.construct --size 1920x1080 --frames 3
construct-bench pathtrace-viewport --scene my-scene.construct --size 1280x720 --seconds 10 --script orbit
construct-bench pathtrace-stages --scene my-scene.construct --size 1280x720 --samples 8
construct-bench material-cost
construct-bench denoise-cost --size 1920x1080 --tile-side 1024 --runs 5
Without --scene, the path-tracing benches render builtin:sphere-lattice,
a lattice of 75 spheres, and material-cost renders builtin:materials-113,
113 spheres with a material each. builtin:sky, a ground plane and three
spheres under the physical sky, is the third scene of the bench set. A
report with the numbers of your scene and of builtin:sphere-lattice on the
same machine shows whether the scene or the machine is slow.
The options that change what the benches measure:
| Option | Benches | Default |
|---|---|---|
--scene <path or builtin:NAME> |
pathtrace-export, pathtrace-viewport, pathtrace-stages, material-cost |
builtin:sphere-lattice, and builtin:materials-113 for material-cost |
--size <WxH> |
All five | 1920x1080 for pathtrace-export and denoise-cost, 1280x720 for the others |
--frames <n> |
pathtrace-export: frames. material-cost: frames per count |
3 and 5 |
--samples <n> |
pathtrace-export: the sample ceiling. pathtrace-stages: samples per sweep. material-cost: samples per count |
1024, 8 and 8 |
--noise-threshold <v> |
pathtrace-export |
0: every frame takes all its samples, so frames compare |
--time-budget-ms <ms> |
pathtrace-export. 0 has no limit |
The setting's default, 10 s |
--environment <id> |
pathtrace-export, pathtrace-stages, material-cost |
The scene's own environment |
--seconds <n> |
pathtrace-viewport |
10 |
--script <still or orbit> |
pathtrace-viewport. orbit moves the camera for 2 s and holds it for 2 s, in turn |
orbit |
--material-records <n> |
pathtrace-stages: bind the scene's spheres in turn to n materials of their own |
The scene's own materials |
--patterns |
pathtrace-stages, with --material-records: each material is a copy of the procedural rusty steel |
Flat materials |
--tile-side <px> |
denoise-cost: the network's largest tile |
1024 |
--runs <n> |
denoise-cost: runs of each denoiser. The first one primes |
5 |
--db <file> |
All five: also write the rows into a SQLite database | None |
A bench exits with code 0 when it measured, 1 when the scene or the render
failed, and 2 when an option is wrong. To compare a frame of your scene with and without
adaptive sampling, run pathtrace-export twice, once with
--noise-threshold 0.01.
Compare a change before and after
construct-bench wgp runs the frame bench on a set of scenes and compares
two runs. It has five verbs:
| Verb | What it does | Platforms |
|---|---|---|
run --label <name> |
Runs frames for each scene, backend and transport. Each row gets the label. The logs go to <out>/<label>/. |
All |
compare --before <label> --after <label> |
Shows the median change between two labels, from raybench framebench report. |
All |
startup --label <name> |
Measures the time from the start of Construct to its first frame. --cold clears the shader caches first. |
Windows |
pacing --label <name> |
Records the frame pacing with PresentMon while it drags the camera, resizes the window or clicks. | Windows |
idle --label <name> |
Records the memory and the video memory of an idle Construct each minute. | Windows |
On another platform, startup, pacing and idle exit with code 2 and do
nothing.
Measure, make the change, build, and measure again:
construct-bench wgp run --label before --build-dir <build> --qt-bin <Qt bin folder>
construct-bench wgp run --label after --build-dir <build> --qt-bin <Qt bin folder>
construct-bench wgp compare --before before --after after --build-dir <build>
--build-dir is the build folder that holds the programs. Without it, the
verbs use the programs in the same folder as construct-bench. --qt-bin
puts the Qt folder first on the search path of each program that the verb
starts. Windows needs it to find the Qt libraries. construct-bench wgp <verb> --help lists all options of a verb.
run stops with exit code 1 if one bench run fails. A bench run that does
not stop in --timeout seconds (900 by default) counts as a failure.
On Windows, Profile Construct on Windows shows how to name the GPU passes for PIX, read the start-up timing lines, and record frame pacing with PresentMon.