eovolkov-ivix created an issue (openstreetmap/mod_tile#501)

## Summary

When `render_list --all` is given `--min-lon/--max-lon/--min-lat/--max-lat`, it 
enqueues fewer
metatiles than the bounding box covers. Tiles in the skipped blocks are never 
rendered, and
`render_list` exits `0` with no warning.

On one real run over the continental USA at z0-8 it enqueued **32** metatiles 
where the box needs
**41** — a 22% shortfall, concentrated at the south and east edges of the box.

Two independent causes, both in the same block of `src/render_list.c`. They are 
separable; the
first accounts for most of the loss.

Version: **0.8.0** (also reproduced by inspection in **v0.8.1** and `master` — 
the relevant lines
are unchanged).

## Cause 1 — the render loop starts at an unaligned coordinate

```c
for (x = min_x; x <= current_max_x; x += METATILE) {
    for (y = min_y; y <= current_max_y; y += METATILE) {
```
<sub>`src/render_list.c`, v0.8.0 lines 468-469 / `master` lines 469-470</sub>

`min_x` and `min_y` come straight from `lon2tilex()` / `lat2tiley()` and are 
arbitrary tile
coordinates, not metatile-aligned. `enqueue()` is then called with them, and 
the coordinate is
rounded down to the containing metatile downstream — which is why rendering 
works at all, and why
the problem is invisible rather than an error.

Because the walk *starts* unaligned, stepping by `METATILE` can step past the 
final block. The loop
runs `floor((max - min) / METATILE) + 1` times, but the box spans
`floor((max - align(min)) / METATILE) + 1` blocks. When `min % METATILE` is 
large enough that the
remainder crosses a boundary, the last row or column is never enqueued.

Worked example, z8, y axis, `min_y = 87`, `max_y = 110`:

```
iterations:  87, 95, 103          (111 > 110, stop)
blocks hit:  80, 88, 96
blocks needed: 80, 88, 96, 104    <-- 104 covers rows 104-110, never enqueued
```

### Suggested fix

```c
for (x = min_x - (min_x % METATILE); x <= current_max_x; x += METATILE) {
    for (y = min_y - (min_y % METATILE); y <= current_max_y; y += METATILE) {
```

Both values are non-negative here, so `%` is well defined. This only ever 
*adds* the partial edge
blocks; no currently-rendered tile changes.

## Cause 2 — `current_max_x` is decremented but `current_max_y` is not

```c
current_max_x = max_x_tmp ? max_x_tmp - 1 : max_x_tmp;
current_max_y = max_y_tmp;
```
<sub>`src/render_list.c`, v0.8.0 lines 460-461 / `master` lines 461-462</sub>

With `lon2tilex()` defined as `floor((lon + 180) / 360 * 2^z)`, `max_x_tmp` is 
the tile *containing*
`max_lon`, so it is part of the box and the `- 1` drops it. The decrement is 
only correct in the
special case where `max_lon` falls exactly on a tile boundary. The latitude 
axis gets no equivalent
adjustment, so the two axes disagree about whether the maximum is inclusive.

Worked example, z8, `max_lon = -66.9`:

```
lon2tilex(-66.9, 8) = floor(80.42) = 80     tile 80 spans lon [-67.5, -66.09], 
contains -66.9
current_max_x       = 79                    tile column 80 excluded
```

### Suggested fix

`current_max_x = max_x_tmp;`, matching the y axis. Where `max_lon` does sit 
exactly on a boundary
this renders one extra column, which seems clearly preferable to dropping a 
needed one — and it
makes the two axes consistent.

## Reproduction

```
render_list --all --map default --num-threads 4 \
  --min-zoom 0 --max-zoom 8 \
  --min-lon -125.0 --max-lon -66.9 --min-lat 24.4 --max-lat 49.4
```

`render_list`'s own log lines give the declared ranges. "Blocks needed" is the 
count of distinct
metatiles covering that range; "enqueued" is what the loop above produces:

| zoom | declared range            | blocks needed | enqueued | short |
|------|---------------------------|---------------|----------|-------|
| 0    | (0, 0) → (0, 0)           | 1             | 1        | —     |
| 1    | (0, 0) → (0, 0)           | 1             | 1        | —     |
| 2    | (0, 1) → (0, 1)           | 1             | 1        | —     |
| 3    | (1, 2) → (1, 3)           | 1             | 1        | —     |
| 4    | (2, 5) → (4, 6)           | 1             | 1        | —     |
| 5    | (4, 10) → (9, 13)         | 2             | 1        | 1     |
| 6    | (9, 21) → (19, 27)        | 4             | 2        | 2     |
| 7    | (19, 43) → (39, 55)       | 6             | 6        | —     |
| 8    | (39, 87) → (79, 110)      | 24            | 18       | 6     |
|      |                           | **41**        | **32**   | **9** |

`render_list` then reports:

```
** Message: Metatiles rendered:
** Message:     32 in 2314.60 seconds (0.01/s)
```

The enqueued column is not measured — it is what Cause 1 predicts — and it 
matches the reported
total of 32 exactly, as well as the per-zoom request counts in the timing 
summary. Counting `.meta`
files on disk afterwards confirms the same gaps.

z7 is unaffected because its remainders happen not to cross a block boundary; 
that is luck, not
correctness.

Note the table understates the total loss, because the declared ranges it is 
computed from are
themselves already clipped by Cause 2 — the z8 row is missing column 80 before 
the loop even runs.

## Impact

The skipped blocks lie along the edges of the requested box, so a pre-render 
pass appears to
succeed while leaving holes exactly where a user panning to the edge of their 
region will land.
With `ModTileMissingRequestTimeout` set, those requests return 404 while the 
render is queued, so
the gap surfaces as intermittent missing tiles well after the pre-render was 
declared complete.


-- 
Reply to this email directly or view it on GitHub:
https://github.com/openstreetmap/mod_tile/issues/501
You are receiving this because you are subscribed to this thread.

Message ID: <openstreetmap/mod_tile/issues/[email protected]>
_______________________________________________
Tile-serving mailing list
[email protected]
https://lists.openstreetmap.org/listinfo/tile-serving

Reply via email to