Research: rain from radar, and compressed ground textures #308

Closed
opened 2026-09-28 23:59:36 +00:00 by jeroen · 0 comments
Owner

Research night 2026-09-29 (unattended, read-only). Two areas, both checked against live services or measured tonight. Neither is in docs/direction.md's frontier, the open issues, or an earlier note (docs/notes/ has no radar or texture-compression note).

1. Rain where it is actually raining: precipitation radar

What. Today the rain is one number for the whole view. systems/map/weather.rs:151-164 fetches met.no's point forecast for the camera and turns the hourly precipitation_amount into WeatherState::precipitation (weather.rs:34, :146). The globe's clouds come from NOAA GFS at 1° resolution (globe_weather.rs:1-12). A radar composite gives the rain as a measured field: showers that start and stop, cross the city, and can be seen as cells from altitude.

Why it fits. Value 1: "Measured beats plausible. Prefer a surveyed fact to a convincing invention." Value 2: "Official sources first." A one-number forecast is the plausible version; the KNMI radar is the measured, official one.

Evidence, checked 2026-09-29 ~00:30 UTC:

  • KNMI Data Platform (api.dataplatform.knmi.nl/open-data/v1/datasets/radar_reflectivity_composites/versions/2.0 and radar_forecast/2.0). The endpoint is live: 401 without a key. With the shared anonymous key published on developer.dataplatform.knmi.nl, both returned {"error":"Rate Limit Exceeded"}, so the anonymous key is not usable in production. A free registered key would be needed, and it would belong on the server (atlas), never in the client. The files are HDF5 on a ~1 km national grid, which is why the decoding belongs in atlas: HDF5 in Rust means binding the C library, and the Android and wasm builds cannot take that. Licence: KNMI open data is published under CC BY 4.0. I did not re-verify this tonight because the dataset page is rendered in JavaScript.
  • RainViewer (api.rainviewer.com/public/weather-maps.json): keyless and live. It returned 13 past frames at 10-minute steps, and a z7 tile over Groningen came back 200 image/png (17 KB). Zoom 7 is the maximum (their docs; z8+ return a 3 KB empty tile, checked), i.e. roughly 600 m per pixel at 512 px. Terms: free with no key, "not intended for high-volume commercial applications", and attribution ("Weather data by RainViewer" plus a link) is required. It is useful as a fallback outside the Netherlands. It is not the primary source (value 2).

What players get. Rain falling where it is actually raining right now, a dry street under a clear patch, and a shower visibly crossing the city. From altitude, rain cells over the country line up with the clouds.

Size. Medium, in two pieces. Atlas: fetch every 5 minutes with a registered key, decode the HDF5, and publish a small georeferenced grid (e.g. a radar/latest PNG with a timestamp), in the same pattern as its other datasets. Client: sample that grid at the camera for precipitation instead of the forecast, keeping met.no as the fallback (value 3). A spatial rain overlay seen from altitude is a later step.

Risks. A registered key is a person's action (an account at KNMI). Radar returns clutter such as ground echo and sea clutter. Near the edge of coverage the composite degrades, so it must fall back to the forecast there and not to zero.

First ticket. Atlas: publish the latest KNMI radar composite as a georeferenced grid, and let the client take its rain from it. Acceptance:

  • A fixture test decodes a recorded composite and samples a known wet and a known dry pixel.
  • --dump-state gains rain_source (radar / forecast), and precipitation equals the grid sample at the anchor.
  • With the grid missing, rain_source == forecast and the value is unchanged from today.

2. Compressed ground textures: most of the ground's GPU memory back

What. Every ground tile is uploaded as uncompressed Rgba8UnormSrgb (systems/map/map_stream.rs:2085, the only TextureFormat in the tile path; there is no BC, ETC2 or ASTC anywhere in crates/cartopolis/src). GPUs sample block-compressed formats natively at 1/4 (BC3/BC7) or 1/8 (BC1) of the memory.

Why it fits. Value 5: "The frame budget is not negotiable. Every streaming path is bounded per frame, in bytes as well as items." Resident memory is the documented way this client loses a device (quality.rs's handset_guarded table; docs/notes/upload-bursts.md).

Evidence.

  • The size of the prize, from quality.rs's handset table: over Groningen, tile_px 512 → 256 moved resident memory 991 → 567 MB. So the ground textures at 512 px come to about 565 MB (the 424 MB saved is 3/4 of that). BC3 would hold them in about 140 MB, and BC1 in about 70 MB. That is the same memory tile_px 256 buys on handsets today, but without halving the ground's resolution.
  • The cost, measured tonight in a scratch crate on this box (shared CPU, release build): texpresso (pure Rust), 512² map tile:
format encode size ratio PSNR
BC1, fast mode (range fit) 18.2 ms 128 KB 8× 39.4 dB
BC1, cluster fit 292.9 ms 128 KB 8× 41.2 dB
BC3, fast mode (range fit) 20.3 ms 256 KB 4× 39.3 dB

Against the 48–68 ms a tile already costs to rasterise (docs/notes/tile-raster-cost.md, cited in CLAUDE.md), the fast encoder adds roughly 30–40 % worker time per tile, and a mip chain adds a third on top. It runs on the worker, not the main thread. The upload per frame also shrinks 4–8×, which helps the per-frame upload ceiling directly.

What players get. On desktop and the Steam Deck: sharper ground at the same memory, or the same ground with several hundred MB of headroom for more of the city to stay loaded. On phones, the same headroom without the 256 px downgrade, if the ETC2/ASTC part below works out.

Size. Medium. Behind a capability check (TEXTURE_COMPRESSION_BC on the adapter; quality::Capabilities is where that kind of answer lives), encode each mip level on the tile worker, upload the compressed format, and keep RGBA8 as the fallback.

Risks.

  • The globe tiles carry the ocean mask in alpha (globe_ocean.rs), and BC1 has only 1-bit alpha, so the globe needs BC3/BC7 while the flat tiles can take BC1.
  • Android GPUs (Adreno) have no BC. They need ETC2 or ASTC, and I did not find a pure-Rust ETC2/ASTC encoder of comparable quality tonight. The C++ ones (etcpak, astc-encoder) cannot go in the Android and wasm builds. So the phone gain is unproven.
  • The web build gets BC only on desktop browsers.
  • The warm tile store (platform::warm) would need to hold compressed chains, which is a win (smaller) but a format change.
  • 39 dB is measured on a 3D capture, not on a flat map raster; flat map colours should compress better. It still needs a look on the Deck (colour is not evidence on lavapipe).

First ticket. Measure first: CARTO_TILE_COMPRESS=bc1|bc3 on the flat clipmap tiles, desktop only, gated on the adapter feature. Acceptance: at Groningen 150 m and 1700 m, --dump-state's gpu_resident_kb drops by at least 50 % against the same run without it, with settled == true and settle time reported beside it (a machine-checkable before/after, value 6). The decision to make it the default comes after the numbers.

Recommendation

Area 2 first. It is a measured performance win on hardware already in the loop (the Deck slot, desktop). It needs no account, key or new service, and its first ticket is a pure before/after number. Area 1 is the more visible one, but it is blocked on a person registering a KNMI key, and it is two pieces of work across atlas and the client. Worth doing once that key exists.

Research night 2026-09-29 (unattended, read-only). Two areas, both checked against live services or measured tonight. Neither is in `docs/direction.md`'s frontier, the open issues, or an earlier note (`docs/notes/` has no radar or texture-compression note). ## 1. Rain where it is actually raining: precipitation radar **What.** Today the rain is one number for the whole view. `systems/map/weather.rs:151-164` fetches met.no's *point forecast* for the camera and turns the hourly `precipitation_amount` into `WeatherState::precipitation` (`weather.rs:34`, `:146`). The globe's clouds come from NOAA GFS at 1° resolution (`globe_weather.rs:1-12`). A radar composite gives the rain as a measured field: showers that start and stop, cross the city, and can be seen as cells from altitude. **Why it fits.** Value 1: *"Measured beats plausible. Prefer a surveyed fact to a convincing invention."* Value 2: *"Official sources first."* A one-number forecast is the plausible version; the KNMI radar is the measured, official one. **Evidence, checked 2026-09-29 ~00:30 UTC:** - **KNMI Data Platform** (`api.dataplatform.knmi.nl/open-data/v1/datasets/radar_reflectivity_composites/versions/2.0` and `radar_forecast/2.0`). The endpoint is live: 401 without a key. With the **shared anonymous key** published on developer.dataplatform.knmi.nl, both returned `{"error":"Rate Limit Exceeded"}`, so the anonymous key is not usable in production. A free registered key would be needed, and it would belong on the server (atlas), never in the client. The files are HDF5 on a ~1 km national grid, which is why the decoding belongs in atlas: HDF5 in Rust means binding the C library, and the Android and wasm builds cannot take that. Licence: KNMI open data is published under CC BY 4.0. I did not re-verify this tonight because the dataset page is rendered in JavaScript. - **RainViewer** (`api.rainviewer.com/public/weather-maps.json`): keyless and live. It returned 13 past frames at 10-minute steps, and a z7 tile over Groningen came back `200 image/png` (17 KB). **Zoom 7 is the maximum** (their docs; z8+ return a 3 KB empty tile, checked), i.e. roughly 600 m per pixel at 512 px. Terms: free with no key, *"not intended for high-volume commercial applications"*, and attribution ("Weather data by RainViewer" plus a link) is required. It is useful as a fallback outside the Netherlands. It is not the primary source (value 2). **What players get.** Rain falling where it is actually raining right now, a dry street under a clear patch, and a shower visibly crossing the city. From altitude, rain cells over the country line up with the clouds. **Size.** Medium, in two pieces. Atlas: fetch every 5 minutes with a registered key, decode the HDF5, and publish a small georeferenced grid (e.g. a `radar/latest` PNG with a timestamp), in the same pattern as its other datasets. Client: sample that grid at the camera for `precipitation` instead of the forecast, keeping met.no as the fallback (value 3). A spatial rain overlay seen from altitude is a later step. **Risks.** A registered key is a person's action (an account at KNMI). Radar returns clutter such as ground echo and sea clutter. Near the edge of coverage the composite degrades, so it must fall back to the forecast there and not to zero. **First ticket.** *Atlas: publish the latest KNMI radar composite as a georeferenced grid, and let the client take its rain from it.* Acceptance: - A fixture test decodes a recorded composite and samples a known wet and a known dry pixel. - `--dump-state` gains `rain_source` (`radar` / `forecast`), and `precipitation` equals the grid sample at the anchor. - With the grid missing, `rain_source == forecast` and the value is unchanged from today. ## 2. Compressed ground textures: most of the ground's GPU memory back **What.** Every ground tile is uploaded as uncompressed `Rgba8UnormSrgb` (`systems/map/map_stream.rs:2085`, the only `TextureFormat` in the tile path; there is no BC, ETC2 or ASTC anywhere in `crates/cartopolis/src`). GPUs sample block-compressed formats natively at 1/4 (BC3/BC7) or 1/8 (BC1) of the memory. **Why it fits.** Value 5: *"The frame budget is not negotiable. Every streaming path is bounded per frame, in bytes as well as items."* Resident memory is the documented way this client loses a device (`quality.rs`'s `handset_guarded` table; `docs/notes/upload-bursts.md`). **Evidence.** - **The size of the prize, from `quality.rs`'s handset table:** over Groningen, `tile_px` 512 → 256 moved resident memory 991 → 567 MB. So the ground textures at 512 px come to about 565 MB (the 424 MB saved is 3/4 of that). BC3 would hold them in about 140 MB, and BC1 in about 70 MB. That is the same memory `tile_px 256` buys on handsets today, but without halving the ground's resolution. - **The cost, measured tonight** in a scratch crate on this box (shared CPU, release build): `texpresso` (pure Rust), 512² map tile: | format | encode | size | ratio | PSNR | |---|---|---|---|---| | BC1, fast mode (range fit) | **18.2 ms** | 128 KB | 8× | 39.4 dB | | BC1, cluster fit | 292.9 ms | 128 KB | 8× | 41.2 dB | | BC3, fast mode (range fit) | **20.3 ms** | 256 KB | 4× | 39.3 dB | Against the 48–68 ms a tile already costs to rasterise (`docs/notes/tile-raster-cost.md`, cited in CLAUDE.md), the fast encoder adds roughly 30–40 % worker time per tile, and a mip chain adds a third on top. It runs on the worker, not the main thread. The upload *per frame* also shrinks 4–8×, which helps the per-frame upload ceiling directly. **What players get.** On desktop and the Steam Deck: sharper ground at the same memory, or the same ground with several hundred MB of headroom for more of the city to stay loaded. On phones, the same headroom without the 256 px downgrade, **if** the ETC2/ASTC part below works out. **Size.** Medium. Behind a capability check (`TEXTURE_COMPRESSION_BC` on the adapter; `quality::Capabilities` is where that kind of answer lives), encode each mip level on the tile worker, upload the compressed format, and keep RGBA8 as the fallback. **Risks.** - The globe tiles carry the ocean mask in **alpha** (`globe_ocean.rs`), and BC1 has only 1-bit alpha, so the globe needs BC3/BC7 while the flat tiles can take BC1. - **Android GPUs (Adreno) have no BC**. They need ETC2 or ASTC, and I did not find a pure-Rust ETC2/ASTC encoder of comparable quality tonight. The C++ ones (etcpak, astc-encoder) cannot go in the Android and wasm builds. So the phone gain is unproven. - The web build gets BC only on desktop browsers. - The warm tile store (`platform::warm`) would need to hold compressed chains, which is a win (smaller) but a format change. - 39 dB is measured on a 3D capture, not on a flat map raster; flat map colours should compress better. It still needs a look on the Deck (colour is not evidence on lavapipe). **First ticket.** *Measure first:* `CARTO_TILE_COMPRESS=bc1|bc3` on the flat clipmap tiles, desktop only, gated on the adapter feature. Acceptance: at Groningen 150 m and 1700 m, `--dump-state`'s `gpu_resident_kb` drops by at least 50 % against the same run without it, with `settled == true` and settle time reported beside it (a machine-checkable before/after, value 6). The decision to make it the default comes after the numbers. ## Recommendation **Area 2 first.** It is a measured performance win on hardware already in the loop (the Deck slot, desktop). It needs no account, key or new service, and its first ticket is a pure before/after number. Area 1 is the more *visible* one, but it is blocked on a person registering a KNMI key, and it is two pieces of work across atlas and the client. Worth doing once that key exists.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#308
No description provided.