Differentiation
RayD's fixed-winner differentiation contract, how each backend selects AD, and which operations support reverse and forward mode.
RayD differentiates the continuous quantities of a query while it holds the discrete choices of the forward pass fixed. This page explains that contract, how you turn AD on in each backend, and which operations support reverse mode (VJP) and forward mode (JVP).
The fixed-winner contract#
Every ray query makes discrete decisions: which triangle a ray hits, which edge is nearest, whether a segment is blocked, which reflectors form a path. RayD makes those decisions in a detached forward pass, then recomputes the continuous result on the winning primitive with the AD-tracked inputs.
- Discrete, no gradient: primitive ids, edge ids, visibility masks, path selection, bounce counts.
- Continuous, differentiable: hit distance
t, hit position, normals, UVs, barycentrics, edge distances and closest points, reflection points and image sources, supported field quantities. - Inputs that receive gradients: vertex positions, mesh and instance transforms, ray origins and directions, query points, SDF values and placement, surfel fields, and material parameters where the operation supports them.
The derivative is therefore the derivative of the result while the same primitive stays the winner. It does not include the change caused by a ray switching to a different triangle, or a silhouette moving across a pixel. Those boundary terms need edge sampling, which you build on top of RayD's edge queries; see Edges and edge sampling.
Turning AD on#
Dr.Jit selects the differentiable overload from the argument types. Pass a RayAD (built from drjit.cuda.ad arrays) to get an IntersectionAD; pass a Ray to get a detached Intersection. Geometry gradients flow when the mesh holds AD arrays, for example after mesh.vertex_positions = vertices with dr.enable_grad(vertices), or when you pass an AD Matrix4f to Scene.set_mesh_transform.
dr.enable_grad(vertices)
mesh.vertex_positions = vertices
# ... build the scene ...
hit = scene.intersect(rd.RayAD(origins, directions))
dr.backward(dr.sum(hit.t)) # reverse mode (VJP)
grad = dr.grad(vertices)Use dr.forward (or dr.forward_from) for forward mode.
Support by operation#
The table shows the 0.8.0 status from api_manifest()["derivatives"], written as VJP / JVP. "Supported" means implemented and tested for that input; "unsupported" means the derivative exists mathematically but RayD does not provide it on that path; "n/a" means the output has no derivative (it is a mask).
| Operation | Inputs | Dr.Jit | Torch |
|---|---|---|---|
intersect | geometry, ray | supported / supported | supported / supported |
nearest_edge (point or ray) | geometry, query | supported / supported | supported / supported |
nearest_edges | geometry, point | supported / supported | supported / supported |
visible, visible_pair, visible_edge, visible_chain | any | n/a | n/a |
trace_reflections | geometry, ray | supported / supported | supported / supported |
trace_refl_epc | all | unsupported | unsupported |
trace_refl_epc_field | geometry, source, receiver, material | supported / supported | unsupported (forward only) |
accumulate_reflections | geometry, source and receiver, material | supported / supported | unsupported (forward only) |
trace_dfr_paths | continuous inputs | supported / supported | unsupported (forward only) |
accum_dfr_direct, accum_dfr | geometry, material | supported / supported | supported / supported |
accum_dfr_coherent_direct | continuous inputs | unsupported | unsupported |
SDF intersect | SDF values, placement, ray | supported / supported | supported / supported |
MixedScene.intersect | selected geometry, ray | supported / supported | supported / supported |
MixedScene.transmittance | surfel fields, ray | supported / supported | supported / supported |
In the Torch backend, an operation marked "forward only" raises RuntimeError when any of its inputs requires a gradient or carries a forward-AD tangent. It never returns a silently detached result. Detach the inputs yourself if you want the primal value only.
Read the status at run time instead of hard-coding it:
status = rt.api_manifest()["derivatives"]["reflection_accumulation"]["accumulate_reflections"]
print(status["input_domains"]["geometry"]) # {'vjp': 'unsupported', 'jvp': 'unsupported'}
print(status["note"]) # why, in one sentencePractical consequences#
- Mask invalid lanes before you reduce. A missed ray has
t = inf; sumdr.select(hit.is_valid(), hit.t, 0)(ortorch.where) instead ofhit.t. - Gradients are exact for the frozen winner, so finite differences agree only while the perturbation keeps the same primitive. Use small steps when you check gradients.
- Visibility results are masks. If your objective depends on occlusion, it is piecewise constant in RayD, and you need an edge-sampling estimator for its gradient.