Fix moire pattern#10518
Conversation
There was a problem hiding this comment.
Welcome to OpenROAD! Thanks for opening your first PR.
Before we review:
- Contribution Guide: https://openroad.readthedocs.io/en/latest/contrib/contributing.html
- Build Instructions: https://openroad.readthedocs.io/en/latest/contrib/BuildWithCMake.html
Please ensure:
- CI passes
- Code is properly formatted
- Tests are included where applicable
A maintainer will review shortly!
There was a problem hiding this comment.
Code Review
This pull request introduces sub-pixel Level of Detail (LOD) thresholds and anti-aliasing techniques to the tile generator to prevent Moiré patterns when zooming out on dense regular arrays. Specifically, it collapses sub-pixel instances to single points, skips fine details for small instances, and implements sub-pixel coverage anti-aliasing for drawing borders. The reviewer suggested optimizing the edge-drawing functions (draw_v_edge and draw_h_edge) by avoiding redundant blendPixel calls when the alpha coverage is zero, and caching the result of std::floor in the coverage lambda to improve rendering performance.
| auto coverage = [](const double v) { | ||
| const double frac = v - std::floor(v); | ||
| return std::make_pair( | ||
| static_cast<int>(std::floor(v)), | ||
| static_cast<unsigned char>(std::lround(frac * 255.0))); | ||
| }; | ||
|
|
||
| auto draw_v_edge = [&](const double xf) { | ||
| const auto [xi, frac_a] = coverage(xf); | ||
| Color near_col = gray; | ||
| Color far_col = gray; | ||
| near_col.a = static_cast<unsigned char>(gray.a - frac_a); | ||
| far_col.a = static_cast<unsigned char>(frac_a); | ||
| for (int iy = loop_yl; iy < loop_yh; ++iy) { | ||
| const int draw_y = (255 - iy); | ||
| setPixel(image_buffer, draw_xl, draw_y, gray); | ||
| blendPixel(image_buffer, xi, draw_y, near_col); | ||
| blendPixel(image_buffer, xi + 1, draw_y, far_col); | ||
| } | ||
| }; | ||
|
|
||
| auto draw_h_edge = [&](const double yf) { | ||
| const auto [yi, frac_a] = coverage(yf); | ||
| Color near_col = gray; | ||
| Color far_col = gray; | ||
| near_col.a = static_cast<unsigned char>(gray.a - frac_a); | ||
| far_col.a = static_cast<unsigned char>(frac_a); | ||
| for (int ix = loop_xl; ix < loop_xh; ++ix) { | ||
| // Cartesian y -> screen y flip happens here; the "far" | ||
| // pixel (yi + 1 in cartesian) is one screen row above. | ||
| blendPixel(image_buffer, ix, 255 - yi, near_col); | ||
| blendPixel(image_buffer, ix, 255 - (yi + 1), far_col); | ||
| } | ||
| }; |
There was a problem hiding this comment.
The current implementation of draw_v_edge and draw_h_edge calls blendPixel twice for every pixel along the edge, even when the sub-pixel coverage alpha (frac_a or 255 - frac_a) is 0. Since blendPixel performs expensive floating-point operations and coordinate transformations, avoiding these redundant calls when the alpha is 0 can significantly improve the rendering performance of the instances-only layer.
Additionally, std::floor(v) is called twice in coverage. We can optimize this by caching the result of std::floor(v).
auto coverage = [](const double v) {
const double floor_v = std::floor(v);
const double frac = v - floor_v;
return std::make_pair(
static_cast<int>(floor_v),
static_cast<unsigned char>(std::lround(frac * 255.0)));
};
auto draw_v_edge = [&](const double xf) {
const auto [xi, frac_a] = coverage(xf);
if (frac_a > 0) {
Color far_col = gray;
far_col.a = frac_a;
for (int iy = loop_yl; iy < loop_yh; ++iy) {
blendPixel(image_buffer, xi + 1, 255 - iy, far_col);
}
}
if (frac_a < 255) {
Color near_col = gray;
near_col.a = static_cast<unsigned char>(gray.a - frac_a);
for (int iy = loop_yl; iy < loop_yh; ++iy) {
blendPixel(image_buffer, xi, 255 - iy, near_col);
}
}
};
auto draw_h_edge = [&](const double yf) {
const auto [yi, frac_a] = coverage(yf);
if (frac_a > 0) {
Color far_col = gray;
far_col.a = frac_a;
for (int ix = loop_xl; ix < loop_xh; ++ix) {
blendPixel(image_buffer, ix, 255 - (yi + 1), far_col);
}
}
if (frac_a < 255) {
Color near_col = gray;
near_col.a = static_cast<unsigned char>(gray.a - frac_a);
for (int ix = loop_xl; ix < loop_xh; ++ix) {
blendPixel(image_buffer, ix, 255 - yi, near_col);
}
}
};…ROAD-Project#10463) Dense regular geometry (flip-chip / 3DBlox microbumps) aliased into a moiré "beat" when zoomed out in the Web GUI 2D viewer. The fix closes both aliasing stages: Server band-limit (tile_generator.cpp): render each tile into a supersampled RGBA buffer (kCoverageSupersample=2) and decimate back with a separable, premultiplied-alpha Lanczos-2 filter (lanczos2Downsample). Drawing primitives take a runtime buffer dimension; fonts, strokes and visibility thresholds scale by super_per_css so output is dpr-invariant. HiDPI: the client sends devicePixelRatio; tiles render at 256*dpr physical pixels (quantizeDpr clamps/snaps it) and Leaflet lays them out at 256 CSS px, so the image maps 1:1 onto the device grid -- no browser resampling re-aliases the raster. Client 1:1 display: buildMapOptions sets zoomSnap/zoomDelta=1 and a floorClampZoom override makes the tile pane upscale-only (never downscale), on both the layout and heatmap layers. Performance: an LRU tile cache on TileGenerator (clean tiles only) and a best-effort render-cancel protocol (kCancel handler, run inline) skip and reuse work during pan/zoom. Tests: an FFT beat-band metric on a central window (the full-tile profile is dominated by a benign edge envelope) plus block-CV; dense sub-pixel array shows no beat, a resolved grid stays sharp, HiDPI renders at 512², and the cache/cancel paths are covered. C++ 45+45 and the JS tile-layer suite pass. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
…AD-Project#10463) After the band-limit, a dense bump array whose pitch is near ~1px still shows a residual moiré beat / busy grid that supersampling alone cannot remove -- only collapsing it to a flat tint does. Add a level-of-detail rule in renderTileBuffer: a bump instance (kPhysBump) that renders smaller than kBumpLodMaxPx (6 CSS px) is not drawn individually. Its footprint is collected and, after the instance loop, the array region is painted as one solid block of the layer color (covering bumps AND the gaps between them), so no grid/beat survives. To avoid slabbing over empty space, a solid block is only used when at least kBumpLodMinCount bumps fall in the tile (a real array); a few scattered bumps are drawn as individual solid rects. On tech-layer tiles a bump is collected only when it actually has obstruction/pin geometry on that layer (bumpHasLayerGeom), so layers where the bump draws nothing are not painted. Bumps >= 6 px keep their resolved, band-limited rendering. Test: a dense COVER_BUMP array at z=0 (bumps ~1-2 px) collapses to a uniform block (central block-CV < 0.03, non-transparent); the existing ResolvedArrayStaysSharp confirms larger bumps still render as structure. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
…AD-Project#10463) The sub-resolution bump LOD block filled the per-tile union of bump bboxes, which stops ~1 pitch short of the tile edge when the tile boundary falls in a gap between bumps. Adjacent tiles then each left a transparent strip at the shared edge, so the dark map background showed through as a persistent black grid aligned to tile boundaries when zoomed in (reproduced at dpr=1, at rest). Snap each block edge that comes within ~1.5x the estimated bump pitch of the tile edge out to that edge, so the blocks of adjacent tiles abut seamlessly where the array continues into the neighbor. A true array edge (bumps far from that side) is left untouched, so empty space beyond the array is not over-filled. Test: BumpBlockTilesAbutWithoutSeam renders an interior tile of a dense COVER_BUMP array and asserts the tile's right-edge column is mostly opaque (the block reaches the edge); without the snap that column is transparent. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
…t#10463) No behavior change (all C++ and JS tests still green); review follow-ups: - Reattach the "Store a Selected..." doc comment to storeSelectable in request_handler.cpp (quantizeDpr had been inserted between them); move the lazy-`app` doc comment back onto createWebSocketTileLayer in websocket-tile-layer.js. - Classify each instance once in renderTileBuffer: add TileVisibility::isCategoryVisible(InstCategory) and have isInstVisible delegate to it, so the render loop no longer calls classifyInstance twice per instance (visibility gate + bump LOD). - Drop selectability (s_* flags and selectable_layers) from the tile cache key — it does not affect the rendered tile, so toggling "selectable" no longer needlessly invalidates the cache. - Simplify lanczos2Downsample: remove the always-full dirty-region window parameters (the dirty-region optimization was never wired up); it now always decimates the whole tile. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
…AD-Project#10463) Post-review improvements (all server-side, tile_generator.cpp): - F3 LOD crossfade: replace the hard 6px bump cut with a band [kBumpLodLoPx=5, kBumpLodHiPx=8]. Below the band a bump is collapsed into the solid block (as before); inside it the bump is ALSO drawn individually and the block's opacity ramps with the mean factor t, so zooming across the threshold fades block<->detail instead of popping. (Under integer zoom the band is hit on at most one level, so it softens rather than fully removes the inter-level step.) - F1 perf: memoize the Lanczos-2 taps by (src_dim,dst_dim) (they depend only on dpr, but were rebuilt with sin/cos every tile); reuse the super-buffer and the Lanczos intermediate via thread_local scratch (renders run one-per-thread) instead of allocating them per tile. - F2 perf: skip the Lanczos decimation entirely when the super-buffer is empty (common while panning) — the output is already a transparent tile and an empty buffer cannot alias. Test: LodCrossfadeFillsGapsInBand renders a bump array sized to the band and asserts the interior stays covered (the faded block tints the gaps), which the old hard cut would not. 48 TileGenerator + 45 RequestHandler green. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
88b9ec2 to
ba7c67f
Compare
Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
|
|
This sounds good from your description. How does it impact render time? Is the LOD only for instances or all object types? @gadfort FYI |
At zoomed-out levels where the LOD is active, there is considerable savings in both time and processing power, as thousands of individual bumps are replaced by simply drawing solid blocks. At more zoomed-in levels where objects are drawn normally, there is a slight increase in backend rendering time due to the 2x supersampling. |

Fix #10463