The Render Loop
Every frame follows a specific sequence. The order matters — polling the assembly snapshot before applying pending mesh data ensures newly generated meshes appear on the same frame their owning rebuild finishes, and updating the camera before rendering ensures smooth animation.
Minimal Render Loop (Standalone)
From viso’s standalone viewer:
#![allow(unused)]
fn main() {
WindowEvent::RedrawRequested => {
let now = Instant::now();
let dt = now.duration_since(last_frame_time).as_secs_f32();
last_frame_time = now;
engine.update(dt);
match engine.render() {
Ok(()) => {}
Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => {
engine.resize(width, height);
}
Err(e) => log::error!("render error: {e:?}"),
}
window.request_redraw();
}
}
What engine.update(dt) Does
engine.update(dt) handles all per-frame coordination work:
- Camera animation tick — interpolates animated focus, distance, and bounding radius; advances turntable auto-rotation.
- Drain the pending Assembly snapshot. If a new
Assemblysnapshot is waiting (because the host orVisoAppcalledengine.set_assembly), the engine rederives its per-entity state and submits aFullRebuildrequest to the background mesh processor. - Apply any pending scene. If the background processor has a
completed
PreparedRebuildready, it is uploaded to the GPU (vertex/index/instance buffers, picking bind groups). GPU upload is typically <1ms.
The main thread never blocks on the background thread. If meshes aren’t ready, the previous frame’s data continues to render until they are.
What engine.render() Does
The render method executes the full pipeline:
- Apply pending animation frame. If an interpolated animation frame is ready from the background thread, upload it to the GPU.
- Tick animation. Advance trajectory and structural animation; submit a new animation-frame request to the background thread if anything changed.
- Update camera and lighting uniforms. Write camera matrices, hovered-residue id, derived fog parameters, and headlamp lighting.
- Frustum cull sidechains.
- Resolve constraints. Translate stored band/pull specs into world-space using current interpolated atom positions.
- Geometry pass. Render all molecular geometry to HDR render
targets (
Rgba16Floatcolor + normals,Depth32Floatdepth). - Picking pass. Render to the offscreen
R32Uinttarget and copy the pixel under the cursor to a staging buffer. - Post-processing. SSAO, bloom, composite (outlines, fog, tone mapping), FXAA.
- Present to the swapchain surface.
- Initiate non-blocking picking readback for next frame.
Frame timing is throttled to a 300 fps target by default
(FrameTiming::should_render short-circuits if the previous frame’s
elapsed time hasn’t met the minimum frame duration).
Error Handling
Surface errors are expected during resize or focus changes:
SurfaceError::Outdated/SurfaceError::Lost— the surface needs reconfiguration. Callresize()with the current window dimensions.- Other errors are logged but non-fatal — the next frame retries.
Rendering to a Texture (Embedding)
If you’re embedding viso in a host that gives you a target texture
view (e.g. a dioxus or egui texture slot), use render_to_texture
instead of render:
#![allow(unused)]
fn main() {
engine.render_to_texture(&texture_view);
}
This runs the same pipeline but writes the final composite to the provided texture view instead of acquiring a swapchain frame, so the caller owns presentation.
Non-Blocking Picking Readback
GPU picking uses a two-frame pipeline to avoid stalling:
- Frame N: The picking pass renders to an offscreen texture and
copies the pixel under the mouse to a staging buffer.
start_readback()initiates an async buffer map. - Frame N+1:
complete_readback()polls the device without blocking. If the map is complete, it reads the residue ID and resolves it to aPickTargetvia the engine’sPickMap. Otherwise it uses the cached value from the previous successful read.
Hover feedback is one frame behind mouse movement, which is imperceptible in practice but avoids GPU pipeline stalls.