Retire regions: capacity-bounded simulator content, geo AOI instead of region scoping #15

Open
opened 2026-07-30 18:15:40 +00:00 by jeroen · 0 comments
Owner

Problem

The project moved from a flat, region-based world (OpenSim/SecondLife style
parcels) to a globe with big_space. Simulator content is no longer bound to
region spaces; it should be capacity-bounded — a simulator caps how many
assets it hosts (prims / objects / scripts), not where they sit.

The concept is retired but the code is not. map_stream.rs:258-261 already says
so in a comment — "Regions are retired (ADR-020/029), so there is no seed region
to read a location from"
— while vibe_sim still seeds one, ships it on the
wire, and the client still meshes it. That gap is actively misleading: it reads
like current design to anyone (or anything) grepping the tree.

It also causes a live replication bug, which is the real cost.
SimWorld::snapshot (crates/vibe_sim/src/state.rs:174-192) filters prims by
whether their region passed the observer AOI test:

let region_ids: HashSet<i64> = regions.iter().map(|r| r.id).collect();
let prims: Vec<PrimDto> = self.prims.iter()
    .filter(|p| region_ids.contains(&p.region_id))

…and regions are filtered by the distance from region_sim_origin to the
observer against aoi_radius (default 500 m, config.rs:37-39). The single
seeded region sits at Vec3::ZERO. So every prim in the world stops
replicating as soon as the observer is more than 500 m from the sim grid origin,
regardless of where the prim actually is.
A geo-distance AOI on the prim's own
GeoAnchor is both the fix and strictly more correct.

Two further symptoms of the same vestige:

  • region_id sent client→server is fabricated: both authoring paths do
    region_query.iter().next().map(|r| r.id).unwrap_or(1) (ui.rs:1686,
    ai_assistant.rs:560). It carries no information.
  • GameState::regions_loaded (resources.rs:237) is write-only — set at
    network.rs:298/304, read by nobody.

Approach

1. vibe_sim — capacity replaces scoping

  • New AOI: filter prims in SimWorld::snapshot (state.rs:174-192) by
    geographic distance from the observer to PrimDto.geo, dropping the region
    hop entirely. This is the bug fix.
  • Capacity limits: there are none todaySimConfig (config.rs:8-22)
    has listen, database_path, tick_hz, aoi_radius, osm_tile_url_template,
    texture_dir and nothing else. Add max_prims (and the fields below), enforced
    in SimWorld::add_prim (state.rs:218-238) before the DB insert, rejected
    through the ServerError path that already exists for this shape at
    net.rs:243-250 (code 400 + message). Wire through Default (config.rs:48-58),
    apply_cli (:71-90) and cli.rs.
  • Avatar spawn (state.rs:70-90) currently takes the lowest-id region's
    origin. Replace with a configured spawn_lat/spawn_lng, falling back to
    Vec3::ZERO — which is already what the unwrap_or yields with no region.
  • Delete regions: Vec<RegionDto> and region_sim_origin from SimWorld
    (state.rs:16-27) and the 300 m grid-layout generator in SimWorld::new
    (:30-67) — the sole producer of RegionDto::sim_*.
  • db.rs: drop seed_default_region (:80-102, called at :76), the region
    half of load_world (:104-131), and the FK existence check in insert_prim
    (:143-153).

2. Schema — migration V7

V1__initial.sql creates regions (:3-15) and gives prims a
region_id INTEGER NOT NULL (:19) with FOREIGN KEY … REFERENCES regions(id)
(:36) plus idx_prims_region (:39). SQLite cannot drop a FK-referenced column
in place, so V7 needs the table-rebuild dance (CREATE TABLE prims_new …; INSERT SELECT; DROP; RENAME). Note PRIM_COLUMNS (db.rs:8) is positional and
row_to_prim (:22-59) indexes off it — dropping the column shifts every index.

3. Wire — protocol v12

Exactly three places carry a region: PrimDto.region_id
(protocol.rs:222-224), NetMessage::WorldSnapshot.regions (:343-349),
NetMessage::CreatePrim.region_id (:354-370). postcard is positional, so
removing them is breaking → append a ProtocolRevision { version: 12, … } row
to PROTOCOL_HISTORY (protocol.rs:29-51)
; PROTOCOL_VERSION derives from it,
so the number is written nowhere else. Roundtrip tests to update: :597, :610,
:693.

4. viberfox client — delete the render chain

components::Region (components.rs:4-16) and Prim.region_id (:22);
rendering::spawn_regions + RegionMesh + update_region_materials
(rendering.rs:16-17, 429-556); tile_loader's RegionTile /
RegionTileTexture / load_region_tiles (tile_loader.rs:86-92, 340-429), which
lose their only producer; debug::debug_region_entities (debug.rs:1-19, the
whole file); GameState::regions_loaded; the region arms in
network::apply_network_snapshot (network.rs:225, 286-303),
data_layers::apply_feature_toggles (data_layers.rs:1109-1118, 1169-1187),
ui::apply_prim_edits (ui.rs:1679-1705), ai_assistant (ai_assistant.rs:381, 532, 560, 573), picking::prim_picking (picking.rs:33, 167-180).

Also drop the six .after(rendering::spawn_regions) constraints (main.rs:640,
:641, :642, :656, :682, :794). None of those systems need regions — they
all anchor off MapStream; the ordering was a "world is ready" barrier. If one is
genuinely wanted, prim_geo::place_geo_prims is the honest barrier.

5. vibe_core — dead tessellation

region_index / region_origin / regions_adjacent / REGION_SIZE
(geo.rs:97-124) have zero call sites outside their own test (:198-207) —
pure deletion, no replacement.

Acceptance criteria

  • A prim replicates based on its own geographic distance from the observer,
    not on a region's. Regression test: an observer >500 m from the sim origin
    still receives a prim next to them.
  • SimConfig carries a prim/object capacity; exceeding it rejects the
    CreatePrim with a ServerError, and there is a test for the boundary.
  • No region in vibe_core::protocol; PROTOCOL_HISTORY has a v12 row and
    the gapless-run test passes.
  • No regions table; V7 migrates an existing data/world.db without data
    loss for prims.
  • grep -rn "Region" crates/ returns only unrelated matches (transit region,
    subregion, hover region, overlap region — listed in Out of scope).
  • Creating a prim still works from both the context menu and the AI assistant.
  • cargo run -p viberfox (solo) starts, streams and renders with no region.

Care points — three things that break quietly

  1. "Create Prim" becomes unreachable. ui.rs:930 gates it on
    context_menu.hit_region_id.is_some(), which is the only load-bearing use of
    hit_region_id. Removing regions without changing that line permanently
    removes prim creation from the map. It needs a new anchor — the click's
    lat_lng (already on ContextMenuState) is the obvious one.
  2. free_camera::get_ground_height (free_camera.rs:1327-1341, called :714
    and :881) clamps the camera against RegionMesh transforms. With none it
    yields 0.0 — same as offline today, so behaviourally safe, but it silently
    stops doing anything. If terrain elevation is coming, this is the hook to
    re-point at the streamed tiles.
  3. REGION_ZOOM_LEVEL (17) must be renamed, not deleted — it is the de-facto
    base map zoom with live callers at map_stream.rs:191, geo_nav.rs:848 and
    db.rs:87. Suggest BASE_MAP_ZOOM. Check REGION_SIZE_METERS
    (world.rs:31, re-exported tile_utils.rs:4) for live consumers before
    deleting.

Verification

cargo check -p vibe_core, cargo check -p vibe_sim, cargo check -p viberfox;
cargo test -p viberfox --bin viberfox, cargo test -p vibe_sim,
cargo test -p vibe_core. Migration: copy data/world.db first (project rule),
then start vibe_sim against a pre-V7 copy and confirm prims survive.
Visual: cargo shots — the region ground quad disappearing must not leave a hole
where the streamed tiles were relying on it.

Out of scope

  • Multi-simulator (peer mesh / directory service) — deferred; auth first.
  • Global f64 / ECEF authoritative positions. Noted as needed eventually
    (positions are cluster-local Vec3 today); not part of removing regions.
  • Scripts. There is no script concept anywhere in vibe_sim or vibe_core
    (grep returns zero hits), so "max scripts per simulator" is greenfield and
    belongs with whatever introduces scripting. This ticket sizes prims/objects.
  • Terrain elevation — see care point 2.
  • Unrelated "region" strings that must stay: globe_weather.rs:204 (subregion API
    param), transit_live.rs:68 (transit region timezone), geo_nav.rs:947
    (regional framing), driver.rs (hover regions), osm_buildings.rs:954 (overlap
    region), docs/guides/server.md:101 (Geofabrik extract).

Branch: feat/15-retire-regions-capacity-bounded

## Problem The project moved from a **flat, region-based world** (OpenSim/SecondLife style parcels) to a **globe with `big_space`**. Simulator content is no longer bound to region spaces; it should be **capacity-bounded** — a simulator caps *how many* assets it hosts (prims / objects / scripts), not *where* they sit. The concept is retired but the code is not. `map_stream.rs:258-261` already says so in a comment — *"Regions are retired (ADR-020/029), so there is no seed region to read a location from"* — while `vibe_sim` still seeds one, ships it on the wire, and the client still meshes it. That gap is actively misleading: it reads like current design to anyone (or anything) grepping the tree. **It also causes a live replication bug, which is the real cost.** `SimWorld::snapshot` (`crates/vibe_sim/src/state.rs:174-192`) filters prims by whether their *region* passed the observer AOI test: ```rust let region_ids: HashSet<i64> = regions.iter().map(|r| r.id).collect(); let prims: Vec<PrimDto> = self.prims.iter() .filter(|p| region_ids.contains(&p.region_id)) ``` …and regions are filtered by the distance from `region_sim_origin` to the observer against `aoi_radius` (default **500 m**, `config.rs:37-39`). The single seeded region sits at `Vec3::ZERO`. So **every prim in the world stops replicating as soon as the observer is more than 500 m from the sim grid origin, regardless of where the prim actually is.** A geo-distance AOI on the prim's own `GeoAnchor` is both the fix and strictly more correct. Two further symptoms of the same vestige: - `region_id` sent client→server is **fabricated**: both authoring paths do `region_query.iter().next().map(|r| r.id).unwrap_or(1)` (`ui.rs:1686`, `ai_assistant.rs:560`). It carries no information. - `GameState::regions_loaded` (`resources.rs:237`) is **write-only** — set at `network.rs:298/304`, read by nobody. ## Approach ### 1. `vibe_sim` — capacity replaces scoping - **New AOI**: filter prims in `SimWorld::snapshot` (`state.rs:174-192`) by geographic distance from the observer to `PrimDto.geo`, dropping the region hop entirely. This is the bug fix. - **Capacity limits**: there are **none today** — `SimConfig` (`config.rs:8-22`) has `listen`, `database_path`, `tick_hz`, `aoi_radius`, `osm_tile_url_template`, `texture_dir` and nothing else. Add `max_prims` (and the fields below), enforced in `SimWorld::add_prim` (`state.rs:218-238`) *before* the DB insert, rejected through the `ServerError` path that already exists for this shape at `net.rs:243-250` (code 400 + message). Wire through `Default` (`config.rs:48-58`), `apply_cli` (`:71-90`) and `cli.rs`. - **Avatar spawn** (`state.rs:70-90`) currently takes the lowest-id region's origin. Replace with a configured `spawn_lat`/`spawn_lng`, falling back to `Vec3::ZERO` — which is already what the `unwrap_or` yields with no region. - Delete `regions: Vec<RegionDto>` and `region_sim_origin` from `SimWorld` (`state.rs:16-27`) and the 300 m grid-layout generator in `SimWorld::new` (`:30-67`) — the sole producer of `RegionDto::sim_*`. - `db.rs`: drop `seed_default_region` (`:80-102`, called at `:76`), the region half of `load_world` (`:104-131`), and the FK existence check in `insert_prim` (`:143-153`). ### 2. Schema — migration V7 `V1__initial.sql` creates `regions` (`:3-15`) and gives `prims` a `region_id INTEGER NOT NULL` (`:19`) with `FOREIGN KEY … REFERENCES regions(id)` (`:36`) plus `idx_prims_region` (`:39`). SQLite cannot drop a FK-referenced column in place, so V7 needs the table-rebuild dance (`CREATE TABLE prims_new …; INSERT SELECT; DROP; RENAME`). Note `PRIM_COLUMNS` (`db.rs:8`) is positional and `row_to_prim` (`:22-59`) indexes off it — dropping the column shifts every index. ### 3. Wire — protocol v12 Exactly three places carry a region: `PrimDto.region_id` (`protocol.rs:222-224`), `NetMessage::WorldSnapshot.regions` (`:343-349`), `NetMessage::CreatePrim.region_id` (`:354-370`). postcard is positional, so removing them is breaking → **append a `ProtocolRevision { version: 12, … }` row to `PROTOCOL_HISTORY` (`protocol.rs:29-51`)**; `PROTOCOL_VERSION` derives from it, so the number is written nowhere else. Roundtrip tests to update: `:597`, `:610`, `:693`. ### 4. `viberfox` client — delete the render chain `components::Region` (`components.rs:4-16`) and `Prim.region_id` (`:22`); `rendering::spawn_regions` + `RegionMesh` + `update_region_materials` (`rendering.rs:16-17, 429-556`); `tile_loader`'s `RegionTile` / `RegionTileTexture` / `load_region_tiles` (`tile_loader.rs:86-92, 340-429`), which lose their only producer; `debug::debug_region_entities` (`debug.rs:1-19`, the whole file); `GameState::regions_loaded`; the region arms in `network::apply_network_snapshot` (`network.rs:225, 286-303`), `data_layers::apply_feature_toggles` (`data_layers.rs:1109-1118, 1169-1187`), `ui::apply_prim_edits` (`ui.rs:1679-1705`), `ai_assistant` (`ai_assistant.rs:381, 532, 560, 573`), `picking::prim_picking` (`picking.rs:33, 167-180`). Also drop the six `.after(rendering::spawn_regions)` constraints (`main.rs:640`, `:641`, `:642`, `:656`, `:682`, `:794`). None of those systems need regions — they all anchor off `MapStream`; the ordering was a "world is ready" barrier. If one is genuinely wanted, `prim_geo::place_geo_prims` is the honest barrier. ### 5. `vibe_core` — dead tessellation `region_index` / `region_origin` / `regions_adjacent` / `REGION_SIZE` (`geo.rs:97-124`) have **zero call sites outside their own test** (`:198-207`) — pure deletion, no replacement. ## Acceptance criteria - [ ] A prim replicates based on its own geographic distance from the observer, not on a region's. Regression test: an observer >500 m from the sim origin still receives a prim next to them. - [ ] `SimConfig` carries a prim/object capacity; exceeding it rejects the `CreatePrim` with a `ServerError`, and there is a test for the boundary. - [ ] No `region` in `vibe_core::protocol`; `PROTOCOL_HISTORY` has a v12 row and the gapless-run test passes. - [ ] No `regions` table; V7 migrates an existing `data/world.db` without data loss for prims. - [ ] `grep -rn "Region" crates/` returns only unrelated matches (transit region, subregion, hover region, overlap region — listed in *Out of scope*). - [ ] Creating a prim still works from both the context menu and the AI assistant. - [ ] `cargo run -p viberfox` (solo) starts, streams and renders with no region. ## Care points — three things that break quietly 1. **"Create Prim" becomes unreachable.** `ui.rs:930` gates it on `context_menu.hit_region_id.is_some()`, which is the *only* load-bearing use of `hit_region_id`. Removing regions without changing that line permanently removes prim creation from the map. It needs a new anchor — the click's `lat_lng` (already on `ContextMenuState`) is the obvious one. 2. **`free_camera::get_ground_height`** (`free_camera.rs:1327-1341`, called `:714` and `:881`) clamps the camera against `RegionMesh` transforms. With none it yields `0.0` — same as offline today, so behaviourally safe, but it silently stops doing anything. If terrain elevation is coming, this is the hook to re-point at the streamed tiles. 3. **`REGION_ZOOM_LEVEL` (17) must be renamed, not deleted** — it is the de-facto base map zoom with live callers at `map_stream.rs:191`, `geo_nav.rs:848` and `db.rs:87`. Suggest `BASE_MAP_ZOOM`. Check `REGION_SIZE_METERS` (`world.rs:31`, re-exported `tile_utils.rs:4`) for live consumers before deleting. ## Verification `cargo check -p vibe_core`, `cargo check -p vibe_sim`, `cargo check -p viberfox`; `cargo test -p viberfox --bin viberfox`, `cargo test -p vibe_sim`, `cargo test -p vibe_core`. Migration: copy `data/world.db` first (project rule), then start `vibe_sim` against a pre-V7 copy and confirm prims survive. Visual: `cargo shots` — the region ground quad disappearing must not leave a hole where the streamed tiles were relying on it. ## Out of scope - **Multi-simulator** (peer mesh / directory service) — deferred; auth first. - **Global f64 / ECEF authoritative positions.** Noted as needed eventually (positions are cluster-local `Vec3` today); not part of removing regions. - **Scripts.** There is no script concept anywhere in `vibe_sim` or `vibe_core` (grep returns zero hits), so "max scripts per simulator" is greenfield and belongs with whatever introduces scripting. This ticket sizes prims/objects. - **Terrain elevation** — see care point 2. - Unrelated "region" strings that must stay: `globe_weather.rs:204` (subregion API param), `transit_live.rs:68` (transit region timezone), `geo_nav.rs:947` (regional framing), `driver.rs` (hover regions), `osm_buildings.rs:954` (overlap region), `docs/guides/server.md:101` (Geofabrik extract). --- Branch: `feat/15-retire-regions-capacity-bounded`
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#15
No description provided.