jw910731

GLM5.1 test 2

May 8th, 2026
64
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 47.65 KB | None | 0 0
  1. # CAGE Scatter Transform Proposal — Claude Sonnet 4 (20250509)
  2.  
  3. ## Section A: Codebase Reconnaissance
  4.  
  5. ### A1. Agent position representation
  6.  
  7. An agent's position is encoded across two GPU-resident data structures and one CPU-side state tracker:
  8.  
  9. **GPU side:**
  10.  
  11. 1. **`AgentState`** (`crates/cage_sim/src/traffic/components.rs:134`) — 64 bytes (16 × u32) per agent. The position-relevant fields are:
  12.   - `progress_packed` (word 3): `[31..16] tail_progress (u16) | [15..0] front_progress (u16)` — normalized position along the current lane, range `[0, 65535]`.
  13.   - `indices_flags` (word 4): `[7..0] locked_front` and `[15..8] locked_tail` — ring buffer indices indicating which `PathSegment` the agent's front and tail occupy.
  14.    - `lane_change_progress` (word 6): `f32` — lateral transition ratio `[0.0, 1.0]` from original to target lane during a lane change.
  15.    - Words 10–15 (`reserved_a` through `reserved_f`) are **completely unused** by the GPU simulation pipeline. `integrate_agents.wgsl` writes back only words 0–9 (lines 741–747).
  16.  
  17. 2. **`PathSegment`** (`components.rs:380`) — 16 bytes ring buffer entry. Position-relevant fields:
  18.    - `packed_path`: `[31..10] LaneID(22) | [9..2] Priority(8) | [1..0] Flags(2)` — identifies the micro-lane.
  19.    - `length: f32` — segment length in meters.
  20.    - `lateral_and_drive`: `[7..0] lateral_mask(u8) | [15..8] is_drive_path(u8) | [23..16] priority_tier(u8) | [25] FLAG_PROBE_FALLBACK | [24] FLAG_PROBE`
  21.    - `start_end_progress`: normalized start/end bounds on this lane.
  22.  
  23.    The existing `lateral_mask` is a **3-bit field** (bits 0–2: left=0x01, right=0x02, self=0x04), set by `encode_path_segments` in `crates/cage_sim/src/road_network/path_encoder.rs:32`. It encodes lane-change adjacency for conflict resolution — it is **not** a cell-occupancy mask. Bits 3–7 of that u8 are currently zero.
  24.  
  25. **CPU side:**
  26.  
  27. 3. **`AgentPathState`** (`crates/cage_sim/src/road_network/path_feeder.rs:22`) tracks the CPU-local ring buffer tail, route step, current lane, and lane change state. It does not track any zone or cell information.
  28.  
  29. 4. **`AgentFeedback`** (`components.rs:276`) — 16 bytes read back from GPU per agent: `indices_flags`, `progress_packed`, `lane_change_progress`, `speed_max_speed`.
  30.  
  31. **Where is `cell_index` and a lateral cell mask currently stored?** Nowhere. Neither concept exists in the codebase. Given existing patterns, `cell_index` would most naturally live in `AgentState.reserved_a` (word 10, currently untouched by all GPU shaders), or be tracked entirely CPU-side alongside `AgentPathState`. The existing 3-bit `lateral_mask` in `PathSegment` serves a different purpose (conflict detection), so a new zone-specific cell mask would be a separate field.
  32.  
  33. **The linear path is the ring buffer** of `PathSegment` entries (depth `QUEUE_DEPTH = 32`, `systems.rs:16`), indexed by `locked_front`/`locked_tail` within `AgentState`. An agent's world position is resolved by reading the `LaneId` from the front segment, looking up `LaneData.arc` from `LaneGraph`, and calling `arc.position_at(s)` where `s = (front_progress / 65535.0) * arc.arc_length`.
  34.  
  35. ### A2. Per-agent GPU transform pipeline
  36.  
  37. CAGE currently has **no GPU-side instanced rendering for agents**. The pipeline is a GPU→CPU→GPU round-trip:
  38.  
  39. 1. **GPU compute** (render graph, `crates/cage_sim/src/traffic/pipeline/render_node.rs:42`): `TrafficCoordinationNode` dispatches 10 compute passes ending with `gather_feedback.wgsl`, which compacts the 64-byte `AgentState` into the 16-byte `AgentFeedback` buffer.
  40.  
  41. 2. **CPU readback** (`crates/cage_sim/src/traffic/agent_upload.rs`): Bevy's `Readback` component copies the feedback buffer to CPU each frame, producing `AgentReadback` data.
  42.  
  43. 3. **CPU Transform update** (`apps/game_client/src/main.rs:557`): `update_from_readback` runs in `TrafficSystemSet::ReadbackSync`. For each agent it:
  44.    - Extracts `locked_front` and `front_progress` from `AgentFeedback`
  45.    - Looks up the `PathSegment` from the CPU mirror `AgentPathInfo.path_queue[agent_id * depth + locked_front]`
  46.    - Extracts `LaneId` from the segment's `packed_path`
  47.   - Calls `lane_data.arc.position_at(s)` to get `[world_X, world_Y, world_Z]`
  48.   - Sets `transform.translation` and `transform.rotation` on the ECS `Transform` component
  49.  
  50. 4. **Bevy PBR draw**: Each agent is a standard Bevy entity with `Mesh3d`, `MeshMaterial3d`, and `Transform` (`main.rs:514–525`). Bevy's default PBR forward renderer handles drawing with automatic instancing.
  51.  
  52. **The integration point for the scatter transform** is `update_from_readback` in `main.rs:557`. After computing the arc-based centerline position (line 630–640), a scatter offset would be added to `transform.translation` based on the agent's zone assignment. This is purely CPU-side, requires no GPU pipeline changes, and slots into the existing `ReadbackSync` system set.
  53.  
  54. **For a future GPU-only path**, `GpuArcGeometry` (`components.rs:767`) is already defined (24 bytes/lane) with the explicit note: "Future use: Upload to a GPU storage buffer for position evaluation in shaders, enabling GPU-side agent positioning without CPU readback." When this path is implemented, the scatter transform would become a compute pass inserted after `gather_feedback` (render_node.rs around line 340) and before the camera driver.
  55.  
  56. ### A3. Road → waiting zone attachment
  57.  
  58. Roads are represented at three levels:
  59.  
  60. 1. **Serialized**: `RoadDef` (`crates/cage_sim/src/road_network/map_format.rs:15`) — has `id: u32`, `centerline: Vec<[f32; 3]>`, `cross_section: CrossSectionDef` (containing `Vec<LaneSpec>`), and `speed_limit: f32`. A `Vec<WaitingZoneDef>` would be added here.
  61.  
  62. 2. **Runtime graph**: `LaneGraph` (`crates/cage_sim/src/road_network/graph.rs:26`) — CSR adjacency over micro-lanes. Each `LaneData` has `road_id: u32`, `cross_section_index: u8`, `arc: ArcGeometry`, and `cumulative_distance: f32`. The `road_lane_starts: HashMap<(u32, u8), LaneId>` on `LaneGraph` maps `(road_id, physical_lane)` to the first micro-lane.
  63.  
  64. 3. **Runtime resource**: `RoadNetworkData` (`crates/cage_sim/src/road_network/mod.rs:48`) — holds `lane_graph`, `road_graph`, `connection_info`, and `road_lane_lengths`. This is the `Resource` inserted at startup by `setup_road_network` (`main.rs:103`).
  65.  
  66. **Where `Vec<WaitingZoneDef>` would be populated:** In the serialized map format, added to `RoadDef`. At map-build time, `build_from_map` (`crates/cage_sim/src/road_network/map_builder.rs:36`) would process these definitions to:
  67. - Resolve `(road_id, start_distance, end_distance, lane_index)` → `Vec<LaneId>` by walking the micro-lane successor chain from `road_lane_starts(road_id, lane_index)` using `cumulative_distance` to find the covering range.
  68. - Pre-bake 2D cell positions from the resolved micro-lanes' `ArcGeometry`.
  69. - Store results in a new `WaitingZoneRegistry` field on `RoadNetworkData` (or as a separate `Resource`).
  70.  
  71. **For scenario-authored maps** (e.g., `highway_merge_map` in `scenarios.rs:32`), `WaitingZoneDef` would be populated inline in the factory function alongside the existing `RoadDef` construction.
  72.  
  73. ---
  74.  
  75. ## Section B: Design Proposal
  76.  
  77. ### B1. Pre-baked data
  78.  
  79. **CPU-side authoring representation** (serialized in `map_format.rs`):
  80.  
  81. ```rust
  82. #[derive(Clone, Debug, Serialize, Deserialize)]
  83. pub struct WaitingZoneDef {
  84.     pub zone_id: u32,
  85.     pub start_distance: f32,       // meters from road start along centerline
  86.     pub end_distance: f32,
  87.     pub affected_lanes: Vec<u8>,   // physical lane indices on this road
  88.     pub max_cells: u8,             // max 64
  89.     pub policy: LateralPolicyDef,
  90.     pub cell_layout: CellLayout,   // how cells are arranged geometrically
  91. }
  92.  
  93. #[derive(Clone, Debug, Serialize, Deserialize)]
  94. pub enum LateralPolicyDef { Shared, Exclusive }
  95.  
  96. #[derive(Clone, Debug, Serialize, Deserialize)]
  97. pub enum CellLayout {
  98.     Grid { rows: u8, cols: u8 },           // Hook Turn Box, bus stop bay
  99.     Spread { lateral_spacing: f32 },        // refuge island (1D spread)
  100. }
  101. ```
  102.  
  103. **CPU-side runtime representation** (built by `build_from_map`):
  104.  
  105. ```rust
  106. #[derive(Clone, Debug)]
  107. pub struct WaitingZone {
  108.     pub zone_id: u32,
  109.     pub lane_ids: Vec<LaneId>,          // micro-lanes belonging to this zone
  110.     pub first_lane_id: LaneId,          // first micro-lane (entry point)
  111.     pub last_lane_id: LaneId,           // last micro-lane (exit point)
  112.     pub entry_cumulative_dist: f32,     // cumulative_distance at zone start
  113.     pub exit_cumulative_dist: f32,      // cumulative_distance at zone end
  114.     pub cells: Vec<[f32; 2]>,           // pre-baked (X, Z) world positions per cell
  115.     pub policy: LateralPolicy,
  116.     pub max_cells: u8,
  117. }
  118.  
  119. #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  120. pub enum LateralPolicy { Shared, Exclusive }
  121.  
  122. #[derive(Resource)]
  123. pub struct WaitingZoneRegistry {
  124.     pub zones: Vec<WaitingZone>,
  125.     pub lane_to_zone: HashMap<LaneId, u32>,    // LaneId -> zone_id
  126.     pub zone_occupancy: Vec<u64>,               // 64-bit bitmap per zone
  127.     pub agent_assignments: HashMap<u32, (u32, u8)>, // agent_id -> (zone_id, cell_index)
  128. }
  129. ```
  130.  
  131. **Pre-baking happens at map-build time** in `build_from_map`. For each `WaitingZoneDef`:
  132. 1. Resolve `affected_lanes` × `[start_distance, end_distance]` → `Vec<LaneId>` by walking `LaneGraph` successors from `road_lane_starts(road_id, lane_idx)`, using `cumulative_distance` as the range criterion.
  133. 2. Compute the arc midpoint for each resolved micro-lane via `ArcGeometry::position_at(arc_length / 2.0)`.
  134. 3. Generate cell 2D positions from the `CellLayout`. For `Grid { rows, cols }`: spread cells in a grid centered on the zone's arc midpoint, oriented perpendicular to the arc tangent. For `Spread`: place cells at equal lateral offsets from the centerline.
  135. 4. Store in `WaitingZone` and insert into `WaitingZoneRegistry`.
  136.  
  137. **GPU-side buffer layout** (for future GPU-only path):
  138.  
  139. ```rust
  140. #[repr(C)]
  141. #[derive(Clone, Copy, Pod, Zeroable)]
  142. pub struct GpuZoneCell {
  143.    pub pos_x: f32,
  144.    pub pos_z: f32,
  145. }
  146.  
  147. #[repr(C)]
  148. #[derive(Clone, Copy, Pod, Zeroable)]
  149. pub struct GpuZoneMeta {
  150.    pub first_cell_offset: u32,   // index into GpuZoneCell[]
  151.    pub num_cells: u32,
  152.    pub entry_lane_id: u32,       // first micro-lane LaneId
  153.    pub exit_lane_id: u32,        // last micro-lane LaneId
  154. }
  155. ```
  156.  
  157. Bound as SSBOs with `STORAGE | COPY_DST`. The cell buffer: `zones[zone_id].cells` flattened into `GpuZoneCell[]`. The meta buffer: one `GpuZoneMeta` per zone.
  158.  
  159. **For the current CPU-side implementation**, only the CPU `WaitingZoneRegistry` is needed — no GPU buffers.
  160.  
  161. **Memory budget for 10,000 zones × 64 cells:**
  162.  
  163. | Item | Per-zone | Total (10K zones) |
  164. |------|----------|-------------------|
  165. | Cell positions | 64 × 8B = 512B | 5.12 MB |
  166. | Zone metadata | ~200B (lane_ids, fields) | 2 MB |
  167. | `lane_to_zone` HashMap | varies, ~200K entries × ~50B | 10 MB |
  168. | Occupancy bitmaps | 8B | 80 KB |
  169. | Agent assignments | ~16B per assigned agent; ~400K concurrent × 16B = 6.4 MB | 6.4 MB |
  170. | **Total CPU** | | **~23.6 MB** |
  171. | GPU cells (future) | 512B | 5.12 MB |
  172. | GPU meta (future) | 16B | 160 KB |
  173. | **Total GPU (future)** | | **~5.3 MB** |
  174.  
  175. ### B2. The GPU transform pass
  176.  
  177. **Current implementation: CPU-side scatter in `update_from_readback`.**
  178.  
  179. Given that the current rendering pipeline is entirely CPU-side (ECS `Transform` components written in `update_from_readback`), the scatter transform is implemented as additional logic in that system, not as a GPU compute pass. This is the correct integration point because:
  180.  
  181. 1. `update_from_readback` already has the agent's current `LaneId`, world position, and heading.
  182. 2. It runs in `ReadbackSync` — the visual layer — not the simulation schedule.
  183. 3. No GPU pipeline changes are required.
  184.  
  185. **Implementation in `update_from_readback`** (after line 640 of `main.rs`):
  186.  
  187. ```rust
  188. // After computing default arc position:
  189. if let Some(registry) = zone_registry {
  190.     if let Some(&zone_id) = registry.lane_to_zone.get(&micro_lane_id) {
  191.         let zone = &registry.zones[zone_id as usize];
  192.         let current_dist = lane_data.cumulative_distance;
  193.         let progress_m = (front_progress as f32 / 65535.0) * lane_data.arc.arc_length;
  194.         let dist_in_zone = (current_dist + progress_m) - zone.entry_cumulative_dist;
  195.         let zone_length = zone.exit_cumulative_dist - zone.entry_cumulative_dist;
  196.         let blend = compute_blend_factor(dist_in_zone, zone_length, BLEND_DISTANCE);
  197.  
  198.         if let Some(&(_, cell_idx)) = registry.agent_assignments.get(&(id as u32)) {
  199.             if (cell_idx as usize) < zone.cells.len() {
  200.                 let cell_pos = zone.cells[cell_idx as usize];
  201.                 let cell_world = Vec3::new(cell_pos[0], transform.translation.y, cell_pos[1]);
  202.                 transform.translation = transform.translation.lerp(cell_world, blend);
  203.             }
  204.         }
  205.     }
  206. }
  207. ```
  208.  
  209. **Future GPU compute pass** (when the codebase migrates to GPU instancing using `GpuArcGeometry`):
  210.  
  211. This would be a new compute shader `scatter_transform.wgsl` dispatched in `TrafficCoordinationNode::run` after `gather_feedback` (render_node.rs, around line 340). It would:
  212.  
  213. - **Read**: `AgentState` (progress, flags), `PathSegment` (LaneId), `GpuArcGeometry[]` (default position), `GpuZoneMeta[]` + `GpuZoneCell[]` (scatter targets), per-agent `(zone_id, cell_index)` buffer.
  214. - **Write**: `InstanceTransform[]` output buffer (packed position + rotation, 32–48 bytes per agent).
  215. - **Bindings** (new bind group layout):
  216.   - `[0]` `AgentState[]` read
  217.   - `[1]` `PathSegment[]` read
  218.   - `[2]` `GpuArcGeometry[]` read
  219.   - `[3]` `GpuZoneMeta[]` read
  220.   - `[4]` `GpuZoneCell[]` read
  221.   - `[5]` `AgentZoneInfo[]` read (per-agent `u32` packed: `zone_id(26) | cell_index(6)`)
  222.   - `[6]` `InstanceTransform[]` read_write
  223.   - `[7]` `ScatterConfig` uniform
  224.  
  225. This requires the `GpuArcGeometry` buffer to be uploaded (currently defined at `components.rs:767` but not yet bound).
  226.  
  227. **Justification for CPU-first**: The current codebase has no GPU-side instance buffer, no custom vertex shader for agents, and no rendering infrastructure beyond Bevy's default PBR. Implementing the scatter as a GPU compute pass today would require building the entire GPU instancing pipeline first — a prerequisite that's out of scope for this feature. The CPU approach works within existing architecture and produces identical visual results.
  228.  
  229. ### B3. cell_index allocation
  230.  
  231. **The constraint**: No new systems in the simulation schedule. No new components on agent entities that the simulation reads. No change to the per-tick GPU inner loop.
  232.  
  233. **The solution**: Extend the existing `refill_agent_path` function (`path_feeder.rs:75`) with zone-aware cell allocation. This is an extension of an existing system, not a new system.
  234.  
  235. **Mechanism**:
  236.  
  237. `refill_agent_path` already walks the micro-lane successor chain and creates `PathSegment` entries (path_feeder.rs:273–320). When it reaches a micro-lane whose `LaneId` is in `lane_to_zone`, it:
  238.  
  239. 1. **Checks if the agent already has a cell** in this zone: look up `agent_assignments.get(&agent_id)`.
  240. 2. **If not, allocates a cell**:
  241.    - `Exclusive`: find the lowest clear bit in `zone_occupancy[zone_id]` using `trailing_ones()`. If the bitmap is all-ones (`0xFFFF_FFFF_FFFF_FFFF`), the zone is full.
  242.    - `Shared`: always use cell 0 (or a round-robin cell for visual variety). No occupancy tracking needed for `Shared` zones.
  243. 3. **Records the assignment**: `agent_assignments.insert(agent_id, (zone_id, cell_index))`.
  244. 4. **If zone is full (Exclusive)**: the path feeder **stops feeding further segments** (breaks out of the refill loop). The agent's ring buffer will drain until it reaches the last fed segment, at which point `remain_distance` drops to zero and the agent decelerates naturally. When a cell is freed (see below), the next call to `refill_agent_path` will succeed and resume feeding.
  245.  
  246. **Cell release** happens in a companion to `update_from_readback`, also in `ReadbackSync`. When an agent's `locked_front` advances past the last zone micro-lane (the segment's `LaneId` is no longer in `lane_to_zone`), release the cell:
  247.  
  248. ```rust
  249. if let Some(&(zone_id, cell_idx)) = registry.agent_assignments.get(&agent_id) {
  250.    let zone = &registry.zones[zone_id as usize];
  251.    if !zone.lane_ids.contains(&micro_lane_id) {
  252.        // Agent has exited the zone
  253.        let old = registry.agent_assignments.remove(&agent_id);
  254.        if zone.policy == LateralPolicy::Exclusive {
  255.            registry.zone_occupancy[zone_id as usize] &= !(1u64 << cell_idx);
  256.        }
  257.    }
  258. }
  259. ```
  260.  
  261. **Why this doesn't violate the constraint**: `refill_agent_path` is an existing function in `PathRefill`. Adding zone-aware conditional logic to it is an extension, not a new system. The GPU simulation pipeline (`integrate_agents.wgsl`, `generate_proposals.wgsl`, `coordinate.wgsl`, etc.) is completely untouched. No new components are read by the simulation. No per-tick inner loop is changed.
  262.  
  263. **Spec discrepancy flagged**: The spec says "the simulation must assign `cell_index` to agents entering a zone." In this design, the assignment happens in the CPU path feeder (`TrafficSystemSet::PathRefill`), not in the GPU simulation pipeline. The GPU simulation is unaware of zones entirely. This is consistent with the architecture's principle that the GPU handles physics/coordination while the CPU handles routing/path planning.
  264.  
  265. ### B4. Smoothstep entry/exit blend
  266.  
  267. **Where the blend factor lives**: Computed on-the-fly in `update_from_readback` from existing state. No per-agent blend state is stored.
  268.  
  269. **The key insight**: The blend factor is a pure function of the agent's **progress through the zone**, which is already available from `front_progress` + `LaneData.cumulative_distance`. No new state needed.
  270.  
  271. **Computation**:
  272.  
  273. ```rust
  274. const BLEND_DISTANCE: f32 = 3.0; // meters, could be per-zone
  275.  
  276. fn compute_blend_factor(dist_in_zone: f32, zone_length: f32, blend_dist: f32) -> f32 {
  277.     let entry_t = smoothstep(0.0, blend_dist, dist_in_zone);
  278.     let exit_start = (zone_length - blend_dist).max(blend_dist);
  279.     let exit_t = 1.0 - smoothstep(exit_start, zone_length, dist_in_zone);
  280.     entry_t.min(exit_t)
  281. }
  282.  
  283. fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
  284.     let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
  285.     t * t * (3.0 - 2.0 * t)
  286. }
  287. ```
  288.  
  289. **How the pass knows the blend state**: The agent's position in the zone is computed from:
  290. - `dist_in_zone = (current_lane.cumulative_distance + progress_meters) - zone.entry_cumulative_dist`
  291. - `zone_length = zone.exit_cumulative_dist - zone.entry_cumulative_dist`
  292.  
  293. Three cases emerge naturally:
  294. - **Entry blend**: `dist_in_zone < BLEND_DISTANCE` → blend ramps from 0 to 1.
  295. - **Full scatter**: `BLEND_DISTANCE <= dist_in_zone <= zone_length - BLEND_DISTANCE` → blend = 1.0.
  296. - **Exit blend**: `dist_in_zone > zone_length - BLEND_DISTANCE` → blend ramps from 1 to 0.
  297.  
  298. **Per-zone vs. per-agent duration**: `BLEND_DISTANCE` is a fixed distance (meters), so the blend duration is `BLEND_DISTANCE / agent_speed` — faster agents blend faster, slower agents blend slower. This is the correct visual behavior (the spatial transition is consistent regardless of speed). If per-zone blend distances are needed, store `blend_distance: f32` in `WaitingZone`.
  299.  
  300. **How this avoids simulation tracking**: The simulation doesn't know about blend states. It simply drives the agent along the lane using `front_progress`. The blend factor is a visual-only computation derived from the same `front_progress` that the simulation already produces. The scatter transform reads this progress and applies the blend — no feedback into simulation state.
  301.  
  302. **Smoothstep vs. linear**: Smoothstep (`3t² - 2t³`) provides C¹ continuity (zero derivative at endpoints), eliminating the visible "jerk" that linear interpolation would produce at blend boundaries.
  303.  
  304. ### B5. LateralPolicy::Exclusive enforcement
  305.  
  306. **Exclusive enforcement happens at cell allocation time (CPU, in `refill_agent_path`)**:
  307.  
  308. When allocating a cell for an `Exclusive` zone:
  309. ```rust
  310. fn allocate_exclusive_cell(occupancy: &mut u64, max_cells: u8) -> Option<u8> {
  311.     let mask = if max_cells == 64 { !0u64 } else { (1u64 << max_cells) - 1 };
  312.     let available = !(*occupancy) & mask;
  313.     if available == 0 {
  314.         return None; // Zone full
  315.     }
  316.     let cell = available.trailing_zeros() as u8;
  317.     *occupancy |= 1u64 << cell;
  318.     Some(cell)
  319. }
  320. ```
  321.  
  322. The `zone_occupancy` bitmap is a `u64` per zone (64 bits = 64 cells). Each bit represents whether a cell is occupied.
  323.  
  324. **When an Exclusive zone is full and another agent attempts to enter:**
  325.  
  326. 1. `refill_agent_path` calls `allocate_exclusive_cell` → returns `None`.
  327. 2. The path feeder breaks out of its refill loop (stops feeding segments for this agent).
  328. 3. The agent's ring buffer drains as the GPU consumes segments. When `locked_front` catches up to `queue_tail`, the agent has no more path to follow.
  329. 4. `integrate_agents.wgsl` sees `locked_front == locked_tail` and `remain_distance` drops to zero. The speed model (`integrate_agents.wgsl` lines 726–736) decelerates the agent.
  330. 5. The agent visually stops at the zone boundary, waiting for a cell to free up.
  331. 6. On the next tick, `refill_agent_path` re-checks. If a cell was freed (another agent exited the zone), allocation succeeds and segments are fed, allowing the waiting agent to enter.
  332.  
  333. **Is there a secondary enforcement at scatter-pass lookup?** No — the scatter pass is purely visual. It reads `cell_index` from `agent_assignments` and applies the offset. If two agents were somehow assigned the same cell in an Exclusive zone (a bug), they would visually overlap. The prevention is entirely at allocation time. A debug assertion could be added: `debug_assert!(zone_occupancy[cell] == true)`.
  334.  
  335. **Shared policy enforcement**: None. Multiple agents can share cell 0. The scatter pass applies the same offset to all of them. For visual variety, `Shared` zones could use a deterministic but non-exclusive cell assignment (e.g., `cell_index = agent_id % max_cells`), spreading agents across cells without capacity limits.
  336.  
  337. ---
  338.  
  339. ## Section C: Reasoning About Design Choices
  340.  
  341. ### C1. Why 6 bits
  342.  
  343. The spec says "6-bit lateral mask." The existing `lateral_mask` in `PathSegment.lateral_and_drive` is a u8 with 3 meaningful bits (left=0x01, right=0x02, self=0x04). These are different concepts — the existing mask is for GPU conflict resolution, the proposed mask is for zone cell occupancy.
  344.  
  345. **6 bits gives you 64 cells** (2⁶ = 64). For the zone types mentioned:
  346.  
  347. | Zone type | Typical cell count | Fits in 6 bits? |
  348. |-----------|-------------------|-----------------|
  349. | Hook Turn Box (motorcycle) | 10–20 bikes | Yes |
  350. | Bus stop bay | 2–3 buses | Yes |
  351. | Refuge island | 6–12 pedestrians | Yes |
  352. | Future: taxi rank | 8–10 taxis | Yes |
  353. | Future: parking row | 20–40 spaces | Yes |
  354.  
  355. **Why not 4 bits (16 cells)?** A large refuge island or parking row could exceed 16 cells. 4 bits would require splitting zones or accepting visual crowding.
  356.  
  357. **Why not 8 bits (256 cells)?** 256 cells per zone is excessive for any realistic waiting area. A 10m × 3m Hook Turn Box fits ~20 motorcycles at 0.5m spacing. Even a large parking lot "zone" (50m × 10m = 500m²) would have at most ~80 spaces at 6m² each. 64 cells (6 bits) covers all reasonable cases.
  358.  
  359. **The 6-bit choice also aligns with the u64 occupancy bitmap**: one u64 = 64 bits = 64 cells. This makes atomic occupancy tracking trivial (single u64 per zone). With 8 bits (256 cells), you'd need 4 × u64 or a `Vec<u64>`, adding complexity.
  360.  
  361. **Cost of 6 bits**: it leaks into the `cell_index` type (u8 with max value 63). If the mask needs to grow (see D2), the occupancy bitmap and cell storage grow accordingly. But 6 bits is the sweet spot for current needs.
  362.  
  363. ### C2. Why pre-bake
  364.  
  365. **Pre-baking 2D cell positions at map-build time vs. computing them every frame on the GPU:**
  366.  
  367. | | Pre-bake | Compute per frame |
  368. |--|----------|-------------------|
  369. | **Per-frame cost** | 1 texture/buffer lookup per agent | Arc evaluation + offset computation per agent |
  370. | **Memory** | 5.12 MB for 10K×64 cells | Zero extra (reuses arc geometry) |
  371. | **GPU complexity** | Simple indexed lookup | Requires arc evaluation in scatter shader |
  372. | **Edit cost** | Re-bake zone (microseconds) | None |
  373.  
  374. **Why pre-bake is correct:**
  375.  
  376. 1. **Zone data changes rarely.** Zones are authored in the editor at design time. A map might have 10K zones, but they're created/edited maybe once per session. Re-baking a single zone's 64 cells is ~microseconds (64 × `ArcGeometry::position_at`). Even re-baking all zones on map load is <1ms.
  377.  
  378. 2. **Lookup is cheaper than computation.** A single `buffer[zone_id * MAX_CELLS + cell_index]` read is simpler and faster than recomputing the perpendicular offset from arc tangent + lateral spacing every frame. At 4M agents × 15 Hz = 60M lookups/sec, the difference matters.
  379.  
  380. 3. **The arc data isn't on the GPU yet.** `GpuArcGeometry` exists but isn't uploaded. Until the GPU-side positioning pipeline is built, the CPU approach needs pre-baked positions anyway.
  381.  
  382. 4. **Authoring flexibility.** Pre-baked positions can be hand-tuned by the designer (e.g., asymmetric layouts, irregular spacing) without changing the shader. Computed positions would be rigid.
  383.  
  384. **Trade-off when zone is edited**: The zone's cells must be re-baked. This triggers from the editor's zone-modification handler, which re-runs the cell layout algorithm for the modified zone. The cost is negligible (microseconds). The `WaitingZoneRegistry` is updated in-place. No GPU re-upload needed for the current CPU path. For the future GPU path, the cell buffer is re-uploaded via `queue.write_buffer`.
  385.  
  386. ### C3. Determinism
  387.  
  388. **The scatter transform does not interact with simulation determinism** because:
  389.  
  390. 1. **No feedback path.** The scatter transform modifies `Transform` components (visual only). The simulation reads `AgentState`, `PathSegment`, `CoordinationResult`, and `cpu_ctrl` — none of which are written by the scatter system.
  391.  
  392. 2. **No simulation state mutation.** The scatter code runs in `ReadbackSync`, which is strictly after `PathRefill` and the GPU compute pipeline. It cannot affect what the GPU sees on the next frame.
  393.  
  394. 3. **Cell allocation is deterministic.** `allocate_exclusive_cell` uses `trailing_zeros()` to find the lowest available bit. Given identical inputs (same agent routing, same zone occupancy), it produces identical cell assignments. The `HashMap` used for `agent_assignments` could introduce nondeterminism if iteration order matters, but it's accessed by key (`agent_id`), so iteration order is irrelevant.
  395.  
  396. **Subtle leak risk**: The `WaitingZoneRegistry` is mutable state shared between `PathRefill` (cell allocation) and `ReadbackSync` (cell release). If `ReadbackSync` were to run before `PathRefill` in a frame (violating the `TrafficSystemSet` ordering), an agent could be assigned a cell that was just released, creating a different assignment order than expected. However, the ordering is enforced by Bevy's system sets (`ConfigSync → PathRefill → ReadbackSync`, `systems.rs:33–40`), so this cannot happen.
  397.  
  398. **Another subtle risk**: If `refill_agent_path` allocates cells based on the GPU readback (which it does — `locked_front` determines free slots), and the GPU readback is delayed by one frame (which it is in the current double-buffered readback scheme), then cell allocation is based on the previous frame's simulation state. This means the simulation is deterministic *given the same sequence of readback values*, which is guaranteed by GPU determinism. No leak.
  399.  
  400. ---
  401.  
  402. ## Section D: Counterfactual / Edge Cases
  403.  
  404. ### D1. Capacity overflow
  405.  
  406. **Scenario**: 64 cells, 80 agents try to enter an Exclusive zone simultaneously.
  407.  
  408. 1. The first 64 agents get cells via `allocate_exclusive_cell` (bits 0–63 set in occupancy bitmap).
  409. 2. Agents 65–80 fail allocation in `refill_agent_path`. The path feeder stops feeding their zone segments.
  410. 3. Their ring buffers drain. The GPU simulation decelerates them. They queue visually behind the zone entry.
  411. 4. As agents 1–64 leave the zone (cells released in `ReadbackSync`), agents 65–80 are allocated cells on subsequent ticks.
  412.  
  413. **User-visible behavior**: Agents queue in a line before the zone. The queue is managed by the ring buffer protocol — agents that arrived first have more segments fed and are closer to the zone, so they get cells first (FIFO within ring buffer consumption order).
  414.  
  415. **For Shared policy**: No overflow possible. All 80 agents get cell 0 (or spread deterministically). The visual shows overlapping/crowded agents in the zone. The simulation doesn't block anyone. If `Shared` zones should have a soft capacity limit (for visual quality), that's a tuning parameter, not a hard constraint.
  416.  
  417. **Are the answers the same?** No. `Exclusive` has hard overflow with queuing. `Shared` has no overflow. This is by design — Exclusive zones represent regulated spaces (bus bays, parking), Shared zones represent open spaces (refuge islands, Hook Turn Boxes where motorcycles can squeeze in).
  418.  
  419. ### D2. Mask growth to 12 bits
  420.  
  421. **Scenario**: A future zone type needs 256+ cells (12 bits = 4096 cells).
  422.  
  423. **Places that need to change:**
  424.  
  425. 1. **`cell_index` type**: Currently `u8` (6 bits used, 2 wasted). Would need `u16`. Affects:
  426.   - `WaitingZoneRegistry.agent_assignments`: `(u32, u8)` → `(u32, u16)`.
  427.   - `WaitingZone.cells: Vec<[f32; 2]>`: grows from 64 to 4096 entries per zone. Memory: 10K × 4096 × 8B = 327 MB. This is a significant increase.
  428.  
  429. 2. **Occupancy bitmap**: `u64` (64 bits) per zone → needs `u64 × ceil(4096/64) = 64 × u64` per zone = 512 bytes. Or `[u64; 64]`. Affects `allocate_exclusive_cell` (multi-word trailing_zeros scan).
  430.  
  431. 3. **`GpuZoneMeta`** (future GPU path): `num_cells` field is already `u32`, so no change needed. `first_cell_offset` is `u32`, fine.
  432.  
  433. 4. **`GpuZoneCell[]`** buffer (future): Grows 64×. 10K × 4096 × 8B = 327 MB on GPU. This is expensive but within modern GPU memory budgets (8–24 GB VRAM).
  434.  
  435. 5. **Per-agent GPU data** (future): The `(zone_id, cell_index)` packed `u32` → would need 12 bits for cell_index, leaving 20 bits for zone_id (max 1M zones). Still fits in a u32.
  436.  
  437. **Is the design future-proof?** Partially. The `cell_index: u8` leaks into several places, but none are structural — they're type-width changes. The `u64` occupancy bitmap is the most fragile assumption; replacing it with a `Vec<u64>` or `[u64; N]` is a contained change.
  438.  
  439. **Was 6 bits the right call?** Yes. For all zone types currently envisioned (Hook Turn Box, bus stop, refuge island), 64 cells is sufficient. Paying the 327 MB memory cost for 4096-cell zones that don't exist yet would be premature. If/when larger zones are needed, the migration path is clear and mechanical. This is a valid instance of "cheap now, expensive later" where "later" may never arrive.
  440.  
  441. ### D3. Mid-blend zone deletion
  442.  
  443. **Scenario**: Player deletes a waiting zone while 30 agents are mid-entry-blend.
  444.  
  445. 1. **Editor action**: Removes `WaitingZoneDef` from the `RoadDef`, triggers re-build of the affected zone's data, removes the zone from `WaitingZoneRegistry`. Alternatively, marks the zone as `deleted = true` (soft delete) for a graceful transition.
  446.  
  447. 2. **Cell release**: The `agent_assignments` entries for the deleted zone are cleared. The occupancy bitmap is zeroed.
  448.  
  449. 3. **Path feeder**: The deleted zone's `lane_to_zone` entries are removed. When `refill_agent_path` next runs for the 30 agents, it no longer sees zone micro-lanes as special — they're just normal road segments. No cell allocation is attempted.
  450.  
  451. 4. **Scatter transform** (`update_from_readback`): The `lane_to_zone` lookup fails (LaneId no longer maps to a zone). The scatter is skipped. Agents snap to the default centerline position computed from arc geometry.
  452.  
  453. 5. **User-visible**: Agents snap from their scattered positions back to the centerline. This is a visual discontinuity, but it's expected — the player just deleted the zone. If a graceful transition is desired, the zone could be "deactivated" (scatter blend factor animated to 0 over 0.5s) before deletion, using a `fade_out` timer on the zone.
  454.  
  455. **Whose responsibility is cleanup?** The **editor** owns cleanup. It must:
  456. - Remove `WaitingZoneDef` from `RoadDef`.
  457. - Trigger `WaitingZoneRegistry` rebuild (or incremental removal).
  458. - Release all `agent_assignments` for the deleted zone.
  459. - The scatter code handles the "zone not found" case gracefully by defaulting to centerline position.
  460.  
  461. The simulation and scatter pass don't need explicit cleanup logic — they respond naturally to the absence of zone data. The path feeder's existing ring buffer protocol handles the transition transparently (agents continue driving on the same micro-lanes, just without scatter offsets).
  462.  
  463. ---
  464.  
  465. ## Section E: Diagnostic
  466.  
  467. ### E1. Two-to-a-cell flicker in Exclusive zones
  468.  
  469. **Candidate 1 (most likely): Race between cell allocation and release in the same frame.**
  470.  
  471. `PathRefill` (cell allocation) and `ReadbackSync` (cell release) run in the same frame, sequentially. If an agent exits a zone and another agent enters the same zone in the same frame, the cell might be released by `ReadbackSync` and then re-allocated to the new agent by `PathRefill` on the next tick. But the exiting agent's last visual frame still has the old `cell_index`, while the entering agent gets the same `cell_index`. For one frame, both agents have the same cell visually.
  472.  
  473. This happens because the ring buffer readback is one frame behind: the exiting agent's `locked_front` is still on the zone segment when `ReadbackSync` runs, but by the time the next frame's `PathRefill` runs, the GPU has advanced past it. The cell release should happen based on the current frame's readback, which it does, but the entering agent was allocated the cell in the same `PathRefill` pass.
  474.  
  475. **Fix**: Ensure cell release happens **before** cell allocation within a frame. The existing ordering is `ConfigSync → PathRefill → ReadbackSync`. Cell release is in `ReadbackSync`. Cell allocation is in `PathRefill`. So allocation happens BEFORE release — this is the wrong order. **Moving cell release to a system that runs before `PathRefill`** (or doing it at the start of `PathRefill`) would fix this.
  476.  
  477. **Candidate 2: GPU readback latency.**
  478.  
  479. The readback data is one frame old. When `ReadbackSync` releases a cell based on last frame's `locked_front`, the agent may have physically moved past the zone on the GPU but the CPU doesn't know yet. The scatter transform uses the stale position for one frame. If another agent was allocated the same cell during this gap, they overlap for one frame.
  480.  
  481. **Distinguishing telemetry**: Log the `agent_id`, `zone_id`, `cell_index`, and `locked_front` for both agents during the overlap frame. If two agents have the same `(zone_id, cell_index)` in `agent_assignments`, it's a race. If only one agent has the assignment but the visual shows overlap, it's stale readback.
  482.  
  483. ### E2. Steppy smoothstep blend
  484.  
  485. **Cause 1 (most likely): Simulation tick rate vs. render frame rate mismatch.**
  486.  
  487. CAGE simulates at 15 Hz but renders at 30+ FPS. The `front_progress` used to compute `dist_in_zone` only updates at 15 Hz (GPU simulation tick). Between simulation ticks, the same `front_progress` is used, causing the blend to advance in discrete 15 Hz steps rather than smoothly.
  488.  
  489. **Telemetry**: Log `front_progress` and computed `blend_factor` every render frame. If `front_progress` changes only every ~66ms (15 Hz), this is the cause.
  490.  
  491. **Fix**: Interpolate `front_progress` between simulation ticks using the render frame's delta time. This requires storing the previous frame's `front_progress` and lerping: `visual_progress = prev_progress + (current_progress - prev_progress) * (render_dt / sim_dt)`. This is purely visual — no simulation state change.
  492.  
  493. **Cause 2: `BLEND_DISTANCE` too short.**
  494.  
  495. If `BLEND_DISTANCE = 3.0m` and the agent moves at 13.9 m/s (50 km/h), the blend completes in 0.22 seconds ≈ 3 simulation ticks. At 15 Hz, that's only 3 distinct blend values — visibly steppy.
  496.  
  497. **Telemetry**: Compute `ticks_in_blend = BLEND_DISTANCE / speed / sim_dt`. If < 5–8, the blend is too fast.
  498.  
  499. **Fix**: Increase `BLEND_DISTANCE` or use a per-zone blend distance scaled by expected speed. For slow zones (bus stops, ≤5 m/s), 3m gives ~9 ticks. For fast approach zones, increase to 5–10m.
  500.  
  501. **Cause 3: Integer precision in `front_progress`.**
  502.  
  503. `front_progress` is `u16` (0–65535) quantizing a `f32` lane progress. For a 5m micro-lane, each step is `5000mm / 65536 ≈ 0.08mm` — plenty of precision. But if the zone spans only one micro-lane (5m) and the blend distance is 3m, the blend range is `3/5 × 65536 ≈ 39321` steps — also plenty. This is unlikely to be the cause unless the zone is extremely short (<0.5m).
  504.  
  505. **Telemetry**: Compute `steps_in_blend = (BLEND_DISTANCE / lane_length) × 65536`. If < 100, precision is the issue. This is extremely unlikely for any reasonable zone geometry.
  506.  
  507. ---
  508.  
  509. ## Section F: Self-Assessment
  510.  
  511. ### F1. Confidence map
  512.  
  513. **High confidence:**
  514. - A1 (agent position representation) — verified from source.
  515. - A2 (GPU transform pipeline) — verified the CPU readback path end-to-end.
  516. - A3 (road representation) — verified `RoadDef`, `LaneGraph`, `LaneData`, `RoadNetworkData`.
  517. - B1 (pre-baked data structures and memory budget) — straightforward given verified types.
  518. - B4 (smoothstep blend from progress) — pure function of existing state, no new infrastructure.
  519. - C3 (determinism) — clear isolation between visual and simulation layers.
  520. - D3 (zone deletion) — natural behavior from the design.
  521.  
  522. **Medium confidence:**
  523. - B2 (GPU transform pass integration point) — the CPU-side approach is solid; the future GPU compute pass description is speculative but consistent with `GpuArcGeometry`'s stated purpose.
  524. - B3 (cell_index allocation in path feeder) — extending `refill_agent_path` is the right hook, but the "stop feeding when zone is full" mechanism may cause edge cases if the ring buffer has other segments queued behind the zone segments. Need to verify that the refill loop's `break` doesn't starve the agent of non-zone segments.
  525. - B5 (Exclusive enforcement) — the allocation-time enforcement is clean, but the release timing (in `ReadbackSync` after `PathRefill`) creates a one-frame window identified in E1.
  526.  
  527. **Low confidence:**
  528. - The exact behavior when the path feeder stops feeding zone segments to a queued agent. The agent decelerates via `remain_distance → 0`, but does it stop gracefully or oscillate? I haven't traced the full `integrate_agents.wgsl` physics model for this edge case.
  529. - Whether `LaneData.cumulative_distance` is correctly populated for all lane types (intersection lanes, etc.). The field exists but I haven't verified the population path for intersection micro-lanes.
  530.  
  531. ### F2. Codebase coverage
  532.  
  533. **Files read directly:**
  534. - `crates/cage_sim/src/traffic/components.rs` (full — AgentState, PathSegment, LaneOccupancy, CoordinationResult, AgentFeedback, GpuArcGeometry, SignalBlockerGpu)
  535. - `crates/cage_sim/src/traffic/systems.rs` (full — TrafficSystemSet, AgentId, QUEUE_DEPTH)
  536. - `crates/cage_sim/src/traffic/pipeline/render_node.rs` (lines 330–380 — post-gather_feedback integration point)
  537. - `crates/cage_sim/src/traffic/pipeline/gpu_buffers.rs` (via explore agent — buffer handles)
  538. - `crates/cage_sim/src/traffic/pipeline/bind_groups.rs` (via explore agent — 10 bind group layouts)
  539. - `crates/cage_sim/src/traffic/pipeline/config.rs` (via explore agent — shader config structs)
  540. - `crates/cage_sim/src/traffic/agent_upload.rs` (lines 160–210 — upload path)
  541. - `crates/cage_sim/src/road_network/map_format.rs` (full — RoadMap, RoadDef, CrossSectionDef, etc.)
  542. - `crates/cage_sim/src/road_network/mod.rs` (full — RoadNetworkData, ConnectionInfo)
  543. - `crates/cage_sim/src/road_network/path_feeder.rs` (lines 1–320 — refill logic)
  544. - `crates/cage_sim/src/road_network/path_encoder.rs` (lines 1–52 — lateral_mask encoding)
  545. - `crates/cage_sim/src/road_network/lane_change.rs` (lines 340–360 — FLAG_MANDATORY_WAIT)
  546. - `crates/cage_core/src/lane.rs` (LaneData, LaneId, LaneType, LateralDirection)
  547. - `crates/cage_core/src/curve.rs` (ArcGeometry, position_at, tangent_at)
  548. - `crates/cage_core/src/lib.rs` (INNER_RADIUS, OUTER_RADIUS)
  549. - `crates/cage_core/src/road.rs` (RoadEnd, TurnType)
  550. - `assets/shaders/traffic/gather_feedback.wgsl` (full)
  551. - `assets/shaders/traffic/integrate_agents.wgsl` (lines 640–749 — mandatory wait, lane change state, writeback)
  552. - `assets/shaders/traffic/common.wgsl` (via explore agent — full struct/function listing)
  553. - `apps/game_client/src/main.rs` (lines 514–650 — spawn, update_from_readback)
  554.  
  555. **Files explored via agents but not read directly:**
  556. - All other WGSL shaders (generate_proposals, coordinate, etc.)
  557. - `crates/cage_render/` (terrain visualization only — confirmed no agent rendering)
  558. - `crates/cage_sim/src/road_network/graph.rs` (LaneGraph CSR structure)
  559. - `crates/cage_sim/src/road_network/map_builder.rs` (build_from_map flow)
  560. - `crates/cage_sim/src/road_network/scenarios.rs` (map factory functions)
  561.  
  562. **Answers that might be wrong as a result:**
  563. - Whether `cumulative_distance` is correctly populated for intersection micro-lanes (it's on `LaneData` but I haven't verified the builder populates it for all lane types). If it's not populated for intersection zones, the blend computation would use wrong values.
  564. - Whether the path feeder can safely break mid-refill when a zone is full without corrupting the ring buffer protocol. I believe it can (the break is identical to the existing capacity-gated break at line 198), but I haven't tested this.
  565.  
  566. ### F3. Hallucination audit
  567.  
  568. **Names I am 100% certain exist in the codebase:**
  569.  
  570. Rust types and structs:
  571. - `AgentState`, `PathSegment`, `LaneOccupancy`, `CoordinationResult`, `AgentFeedback`, `GpuArcGeometry`, `SignalBlockerGpu` — all in `components.rs`
  572. - `AgentShaderConfig`, `TrafficShaderConfig`, `SignalShaderConfig` — in `pipeline/config.rs`
  573. - `LaneData`, `LaneId`, `LaneType`, `LateralDirection` — in `cage_core/src/lane.rs`
  574. - `ArcGeometry` — in `cage_core/src/curve.rs`
  575. - `RoadDef`, `RoadMap`, `CrossSectionDef`, `LaneSpec`, `IntersectionDef`, `IntersectionArm`, `LaneConnection`, `SignalTimingDef`, `SignalPhase` — in `map_format.rs`
  576. - `LaneGraph`, `RoadGraph`, `LaneSuccessor`, `LateralNeighbor` — in `graph.rs`
  577. - `RoadNetworkData`, `ConnectionInfo` — in `road_network/mod.rs`
  578. - `AgentPathState`, `AgentPathInfo` — in `path_feeder.rs`
  579. - `TrafficSystemSet`, `AgentId`, `TrafficParticipant`, `QUEUE_DEPTH` — in `systems.rs`
  580. - `INNER_RADIUS`, `OUTER_RADIUS` — in `cage_core/src/lib.rs`
  581. - `RoadEnd`, `TurnType`, `LaneRequirement` — in `cage_core/src/road.rs`
  582. - `AgentGpuUpload` — in `agent_upload.rs`
  583. - `TrafficCoordinationNode` — in `render_node.rs`
  584. - `LaneChangeState`, `CpuLaneChangePhase` — in `lane_change.rs`
  585.  
  586. Constants:
  587. - `FLAG_PROBE_BIT`, `FLAG_PROBE_FALLBACK_BIT`, `FLAG_PROBE_PROPOSAL_BIT`, `FLAG_MANDATORY_WAIT_BIT` — in `components.rs`
  588. - `AGENT_LC_STATE_MASK`, `AGENT_LC_STATE_NONE/PROBING/TRANSITION/DONE` — in `components.rs`
  589. - `MIN_LANE_CHANGE_DISTANCE`, `CAR_BODY_METERS`, `MIN_LC_DIST` — in WGSL and/or Rust
  590. - `LaneId::MAX` = `(1 << 22) - 1`
  591.  
  592. Functions/methods:
  593. - `ArcGeometry::position_at(s)`, `ArcGeometry::tangent_at(s)`, `ArcGeometry::parallel_offset(d)` — in `curve.rs`
  594. - `AgentState::pack_progress`, `unpack_front_progress`, `unpack_locked_front`, `pack_cpu_ctrl` — in `components.rs`
  595. - `PathSegment::pack_lateral_drive` — in `components.rs`
  596. - `encode_path_segments` — in `path_encoder.rs`
  597. - `refill_agent_path` — in `path_feeder.rs`
  598. - `build_from_map` — in `map_builder.rs`
  599. - `find_route` — in `pathfinding.rs`
  600.  
  601. WGSL shaders:
  602. - `common.wgsl`, `generate_proposals.wgsl`, `generate_signal_proposals.wgsl`, `count_proposals.wgsl`, `prefix_sum.wgsl`, `scatter_proposals.wgsl`, `coordinate.wgsl`, `reset_lanes.wgsl`, `reset_results.wgsl`, `integrate_agents.wgsl`, `gather_feedback.wgsl`, `terrain.wgsl`
  603.  
  604. **Names I am NOT certain exist (may be fabricated or misremembered):**
  605.  
  606. - `AgentPathStates` — I saw this name in the `pub use` of `road_network/mod.rs:22`. I'm fairly confident it exists but didn't read the definition.
  607. - `LaneIdAllocator` — read in `lane.rs`, it exists, but I didn't verify its methods.
  608. - `SpatialIndex` — mentioned by explore agent in `spatial_index.rs`, I didn't verify the struct name.
  609. - `TerrainConfigUniform`, `TerrainVizPlugin`, `TerrainMaterial` — from `cage_render`, mentioned by explore agent. I'm confident they exist given the shader file.
  610. - `TrafficGpuBuffers` — from `gpu_buffers.rs`, mentioned by explore agent. I didn't verify the exact struct name.
  611. - `TrafficPipelines`, `TrafficBindGroups` — from `bind_groups.rs`, mentioned by explore agent. I didn't verify the exact struct names.
  612. - `DebugReadback` — from `render_node.rs`, mentioned by explore agent. I didn't verify.
  613. - `AgentReadback` — used in `main.rs:558`, I read the usage but not the definition.
  614. - `AgentStateBufferHandle`, `ResultsBufferHandle`, `FeedbackBufferHandle` — from `gpu_buffers.rs`, mentioned by explore agent.
  615. - `NUM_BATCHES` in `agent_upload.rs` — mentioned by explore agent.
  616. - `WORKGROUP_SIZE = 256` in `config.rs` — mentioned by explore agent.
  617.  
  618. **Names that definitely DO NOT exist (new proposals):**
  619. - `WaitingZoneDef`, `LateralPolicyDef`, `CellLayout`, `WaitingZone`, `LateralPolicy`, `WaitingZoneRegistry`, `GpuZoneCell`, `GpuZoneMeta`, `compute_blend_factor`, `smoothstep`, `allocate_exclusive_cell` — these are all proposed new types/functions.
  620.  
  621. ### F4. Constraint check
  622.  
  623. **Hard constraint**: "The simulation pipeline must not be modified. No new systems in the simulation schedule, no new components on agent entities that the simulation reads, no change to the per-tick inner loop."
  624.  
  625. **Analysis:**
  626.  
  627. | Aspect | Changed? | Details |
  628. |--------|----------|---------|
  629. | GPU compute shaders | No | `integrate_agents.wgsl`, `generate_proposals.wgsl`, `coordinate.wgsl`, etc. are untouched. |
  630. | `AgentState` GPU layout | No | Reserved fields (words 10–15) remain unused by GPU. No GPU shader reads or writes them. |
  631. | GPU buffer bindings | No | No new bind groups, no new buffers in the compute pipeline. |
  632. | Simulation schedule systems | No new systems | `TrafficSystemSet` has exactly three variants: `ConfigSync`, `PathRefill`, `ReadbackSync`. No new variant added. |
  633. | Agent ECS components (sim reads) | No | The simulation reads `AgentId`. No new component read by simulation. |
  634. | Per-tick inner loop | No | The GPU physics loop (`integrate_agents.wgsl`) is unchanged. |
  635. | `refill_agent_path` function | Extended | Logic added to an existing function in `PathRefill`. This is the one bend. |
  636.  
  637. **The bend**: I extended `refill_agent_path` (in `PathRefill`) with zone-aware cell allocation and segment-emission gating. This is an extension of an existing system, not a new system. The extension:
  638. - Does not change the function signature or return type.
  639. - Does not add new ECS components or resources that the simulation pipeline reads.
  640. - Does not modify the ring buffer protocol.
  641. - Operates on the same `AgentPathState` and `PathSegment` types.
  642. - Uses the existing `FLAG_MANDATORY_WAIT` mechanism (already defined at `components.rs:425`, already consumed by `integrate_agents.wgsl:668`).
  643.  
  644. I believe this is the smallest possible extension consistent with the constraint's spirit. The alternative — putting cell allocation in `ReadbackSync` (visual layer only) — would create a race condition where the path feeder feeds zone segments before the cell is allocated, and the scatter transform wouldn't know which cell to use.
  645.  
  646. **If even this extension is considered a violation**, the fallback is: move all cell allocation to `ReadbackSync`, accepting a one-frame visual delay (the scatter offset appears one frame after the agent enters the zone). This is purely visual and does not touch `PathRefill` at all.
  647.  
Add Comment
Please, Sign In to add comment