From 516e83ac14732f402c7ffff18e307a4d1f67fc29 Mon Sep 17 00:00:00 2001 From: Lukas Lipp Date: Tue, 8 Sep 2026 13:01:14 +0200 Subject: [PATCH 1/3] ``PSIntegrator``: fix boundary sample normalization Boundary splats were scaled by ``1 / spp``, which assumes a wavefront of ``prod(crop_size) * spp``. When the film samples its border, ``prepare()`` launches ``prod(crop_size + 2 * border) * spp`` samples instead, which overestimates the derivative by 1.56x for a 16x16 film with the default Gaussian reconstruction filter. These image blocks are splatted with ``weight = 0`` and hence need an explicit sample count. Boundary samples are spread over the crop window, so the correct factor is ``prod(crop_size) / wavefront_size``. It reduces to ``1 / spp`` if the film does not sample its border. --- src/python/python/ad/integrators/common.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/python/python/ad/integrators/common.py b/src/python/python/ad/integrators/common.py index 195e6b807..f9a8116f0 100644 --- a/src/python/python/ad/integrators/common.py +++ b/src/python/python/ad/integrators/common.py @@ -1140,7 +1140,10 @@ def render_primarily_visible_silhouette(self, block.put( pos=sensor_ds.uv, wavelengths=wavelengths, - value=derivative * dr.rcp(mi.ScalarFloat(spp)), + # Boundary samples are not tied to pixels: normalize by their + # count and the pixel area, not by `spp` + value=derivative * (dr.prod(film.crop_size()) / + mi.ScalarFloat(sampler.wavefront_size())), weight=0, alpha=1, active=active @@ -1246,8 +1249,10 @@ def render_indirect_silhouette(self, scene, sensor, sample, sampler, preprocess=False) active = dr.any(value != 0) - # Account for the guiding sampling density and spp - value *= rcp_pdf_guiding * dr.rcp(spp) + # Account for the guiding density, the sample count and the + # pixel area + value *= rcp_pdf_guiding * (dr.prod(film.crop_size()) / + mi.ScalarFloat(sampler.wavefront_size())) # Splat the result to the film block = film.create_block(normalize=True) From 591dc50b56b42ddaaf4efdcd1a86de4fba89173a Mon Sep 17 00:00:00 2001 From: Lukas Lipp Date: Tue, 8 Sep 2026 13:01:19 +0200 Subject: [PATCH 2/3] ``PSIntegrator``: account for the filter border when culling Silhouette points projecting outside of the crop window were discarded, although the reconstruction filter reaches ``border_size`` pixels beyond it and the primal render does account for them. Widen the frustum test by that amount when the film samples its border. --- src/python/python/ad/projective.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/python/python/ad/projective.py b/src/python/python/ad/projective.py index 4c8b6fede..2e312082c 100644 --- a/src/python/python/ad/projective.py +++ b/src/python/python/ad/projective.py @@ -175,7 +175,17 @@ def eval_primary_silhouette_radiance_difference(self, it = dr.zeros(mi.Interaction3f) it.p = ss.p ds, _ = sensor.sample_direction(it, mi.Point2f(0), active) - visible &= ds.pdf != 0 + film = sensor.film() + if film.sample_border(): + # The filter reaches past the crop window, so points just + # outside of it still contribute + border = film.rfilter().border_size() + size = mi.ScalarVector2f(film.crop_size()) + in_front = (to_world.inverse() @ ss.p).z > 0 + visible &= in_front & dr.all((ds.uv >= -border) & + (ds.uv <= size + border)) + else: + visible &= ds.pdf != 0 # Sample wavelengths wavelength_sample = 0 From b9351f0a369f791798940081dc3b97ba5a0eb039 Mon Sep 17 00:00:00 2001 From: Lukas Lipp Date: Wed, 9 Sep 2026 19:03:19 +0200 Subject: [PATCH 3/3] ``PSIntegrator``: splat boundary samples at the right pixel ``Sensor::sample_direction`` returns ``ds.uv`` relative to the crop window, while ``ImageBlock::put`` subtracts the block's own offset from the position it is handed. The boundary splats passed ``ds.uv`` straight through, so with a non-zero ``crop_offset`` the discontinuous derivatives landed ``crop_offset`` pixels away from the edge they belong to. ``sample_rays`` already adds the offset back for the primal render, and ``ptracer`` does the same for its particle splats. --- src/python/python/ad/integrators/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/python/ad/integrators/common.py b/src/python/python/ad/integrators/common.py index f9a8116f0..65067b7a1 100644 --- a/src/python/python/ad/integrators/common.py +++ b/src/python/python/ad/integrators/common.py @@ -1138,7 +1138,7 @@ def render_primarily_visible_silhouette(self, block = film.create_block(normalize=True) block.set_coalesce(block.coalesce() and spp >= 4) block.put( - pos=sensor_ds.uv, + pos=sensor_ds.uv + mi.ScalarPoint2f(block.offset()), wavelengths=wavelengths, # Boundary samples are not tied to pixels: normalize by their # count and the pixel area, not by `spp` @@ -1258,7 +1258,7 @@ def render_indirect_silhouette(self, block = film.create_block(normalize=True) block.set_coalesce(block.coalesce() and spp >= 4) block.put( - pos=sensor_uv, + pos=sensor_uv + mi.ScalarPoint2f(block.offset()), wavelengths=wavelengths, value=value, weight=0,