From 8ea32012fe21989782688b168331616d01448263 Mon Sep 17 00:00:00 2001 From: Mark Allyn Date: Sun, 10 May 2026 09:09:09 -0700 Subject: [PATCH] clean out test files --- CMakeLists.txt | 107 --------- DESIGN.md | 606 ------------------------------------------------- 2 files changed, 713 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 876a2cd..e69de29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,107 +0,0 @@ -# MIT License -# Author: Mark Allyn -# -# CMakeLists.txt — Museum Vintage Radar Exhibit -# -# Build: -# cd build && cmake .. && make -j$(nproc) -# -# Run (from project root): -# ./build/radar - -cmake_minimum_required(VERSION 3.16) -project(radar LANGUAGES C CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -# ---------------------------------------------------------------- -# Find packages -# ---------------------------------------------------------------- -find_package(OpenGL REQUIRED) -find_package(Freetype REQUIRED) -find_package(Threads REQUIRED) - -# GLFW — prefer system package; fall back to find_library -find_package(PkgConfig QUIET) -if(PkgConfig_FOUND) - pkg_check_modules(GLFW glfw3) -endif() -if(NOT GLFW_FOUND) - find_library(GLFW_LIBRARIES NAMES glfw glfw3 REQUIRED) - find_path(GLFW_INCLUDE_DIRS GLFW/glfw3.h REQUIRED) -endif() - -# ---------------------------------------------------------------- -# GLAD (compiled directly from source) -# ---------------------------------------------------------------- -set(GLAD_SRC ${CMAKE_SOURCE_DIR}/glad/src/glad.c) -set(GLAD_INC ${CMAKE_SOURCE_DIR}/include) - -# ---------------------------------------------------------------- -# Source files — main radar binary -# ---------------------------------------------------------------- -set(SOURCES - src/main.cpp - src/shared_render_state.cpp - src/target_buffer.cpp - src/phosphor.cpp - src/graticule.cpp - src/left_panel.cpp - src/scope.cpp - src/scope_manager.cpp - src/scope_intro.cpp - src/scope_ppi.cpp - src/scope_marine_ppi.cpp - src/simulator.cpp - src/traffic_cop.cpp - src/knob_panel.cpp - src/rpi_receiver.cpp - ${GLAD_SRC} -) - -add_executable(radar ${SOURCES}) - -target_include_directories(radar PRIVATE - src/ - ${GLAD_INC} - ${FREETYPE_INCLUDE_DIRS} - ${GLFW_INCLUDE_DIRS} -) - -target_link_libraries(radar PRIVATE - OpenGL::GL - Freetype::Freetype - Threads::Threads - ${GLFW_LIBRARIES} -) - -# Compiler warnings -target_compile_options(radar PRIVATE - -Wall -Wextra -Wpedantic - -Wno-unused-parameter -) - -# ---------------------------------------------------------------- -# terrain_preprocess — offline tool, links GDAL, NOT part of radar -# Uncomment and install libgdal-dev to build this target. -# ---------------------------------------------------------------- -# find_package(GDAL) -# if(GDAL_FOUND) -# add_executable(terrain_preprocess src/terrain_preprocess.cpp ${GLAD_SRC}) -# target_include_directories(terrain_preprocess PRIVATE src/ ${GLAD_INC} ${GDAL_INCLUDE_DIRS}) -# target_link_libraries(terrain_preprocess PRIVATE ${GDAL_LIBRARIES} Threads::Threads) -# endif() - -# ---------------------------------------------------------------- -# Copy shaders and data to build directory for in-build-dir running -# ---------------------------------------------------------------- -add_custom_target(copy_assets ALL - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/shaders ${CMAKE_BINARY_DIR}/shaders - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/data ${CMAKE_BINARY_DIR}/data - COMMENT "Copying shaders and data to build directory" -) -add_dependencies(radar copy_assets) diff --git a/DESIGN.md b/DESIGN.md index 9a72f74..e69de29 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,606 +0,0 @@ -# Radar Simulation — Class Design and File Layout -Author: Mark Allyn - ---- - -## Class Hierarchy - -``` -Scope (abstract base) -├── ExhibitIntro -├── AScope (abstract) -│ ├── MarineAScope -│ └── ChainHomeAScope -├── PPIScope (abstract) -│ ├── MarinePPIScope -│ └── ATCPPIScope -└── PARScope -``` - ---- - -## Class Descriptions - -### Scope (abstract base) -Everything all scopes share: -- Left panel text rendering -- `s` / `S` key handling (scope advance / reverse) -- Auto-advance timer reset on any key or control input -- Pure virtual methods: `render()`, `handleKey()`, `getDescription()` - -### ExhibitIntro : public Scope -- Text-only rendering, no radar display -- Header: "WELCOME TO MUSEUM VINTAGE RADAR EXHIBIT" (all caps) -- Emphasizes s/S keys and 120-second auto-advance - -### AScope : public Scope (abstract) -Shared A-scope behavior: -- Horizontal range axis, vertical amplitude axis -- Noise floor rendering (rain/wave clutter) -- Incandescent graticule (three horizontal amplitude lines + vertical range lines) -- Bearing control with key-hold acceleration -- Phosphor type as parameter (P1 or P7) - -### MarineAScope : public AScope -- P1 phosphor (green) -- Range settings: 2, 4, 6 miles - - Max 2: one interim range at 1 - - Max 4: one interim range at 2 - - Max 6: one interim range at 4 -- Keys: c (bearing CW), v (bearing CCW), u (range up), d (range down) - - u and d are ignored during graticule swap animation - -**Graticule swap animation:** -In the period, changing max range required the operator to physically slide the glass -graticule panel upward and out from in front of the CRT, then slide the replacement -graticule (calibrated for the new range) downward into position. This is simulated. - -State machine triggered by u or d key: - - NORMAL graticule in place, scope operating normally - | - | u or d pressed - v - SLIDING_OUT old graticule translates upward off screen (~0.5 seconds) - | - v - BARE_CRT no graticule rendered; CRT trace and noise floor still running - | - v - SLIDING_IN new graticule slides down into position (~0.5 seconds) - | - v - NORMAL new range now active - -- u and d keys ignored while state != NORMAL -- Graticule remains incandescent color throughout (edge-lit glass, not CRT-dependent) - -### ChainHomeAScope : public AScope -- P7 phosphor (early implementation, slow decay for slow PRF) -- Goniometer state: H/V mode toggle, azimuth angle, elevation angle -- PRF toggle: 25 Hz / 12.5 Hz -- Calibrator stretch/shrink scale factor -- Fixed 100-mile range (no range change) -- Keys: [ (goniometer H/V toggle), 9 (tune left), 0 (tune right), - . (PRF toggle), n (calibrator shrink), m (calibrator stretch) - -### PPIScope : public Scope (abstract) -Shared PPI behavior: -- Clockwise sweep with P7 phosphor persistence - - Immediate beam strike: blue - - Persistence: green/yellow, faded by next sweep pass -- Incandescent bearing graticule: - - Inner ring with 1-degree tick marks (0–359, True North = 0) - - Text labels every 15 degrees - - Outer ring -- Yellow cursor: 10-degree arc section + bearing crossline -- Cursor range/bearing readout displayed under scope (white text) -- Bearing offset for boat mode (k = right, j = left) -- Keys: r (cursor bearing right), l (cursor bearing left), - t (cursor range increase), y (cursor range decrease), - k (antenna bearing offset right), j (antenna bearing offset left) -- Cursor range clamped to max range if exceeded - -### MarinePPIScope : public PPIScope -- Sweep time: 4 seconds -- Max range settings: 2, 4, 6 miles - - Max 2: rings at 1, 2 - - Max 4: rings at 2, 4 - - Max 6: rings at 4, 6 -- Keys: u (range up), d (range down) — affects only this scope - -### ATCPPIScope : public PPIScope -- Sweep time: 5 seconds -- Max range settings: 5, 10, 15, 20 miles - - Max 5: rings at 2.5, 5 - - Max 10: rings at 2, 4, 6, 8, 10 - - Max 15: rings at 4, 8, 12, 15 - - Max 20: rings at 5, 10, 15, 20 -- Keys: u (range up), d (range down) — affects only this scope - -### PARScope : public Scope -- Two vertically stacked sub-scopes (right panel): - - Top: azimuth (lateral deviation vs. range) — ~1/3 larger - - Bottom: elevation (vertical deviation vs. range) -- P7 phosphor; graticules are incandescent etched glass -- 30 Hz alternating scan between azimuth and elevation planes - (each plane scans at ~15 Hz, i.e., 1/15 second per plane) -- Fixed 10-mile range — no range change control -- Non-linear horizontal scale: inner 5 miles occupies 70% of width -- All targets simulated; no cursor or bearing controls -- Located at south end of Runway 16/34, BLI — active runway 34 (northbound) - ---- - -## Supporting Classes - -| Class | Thread | Purpose | -|---|---|---| -| `ScopeManager` | 1 | Owns scope list; handles s/S switching and 120s auto-advance timer | -| `PhosphorRenderer` | 1 | P1 and P7 decay/persistence simulation; shared dependency | -| `Graticule` | 1 | Draws incandescent graticule lines and text; parameterized per scope | -| `LeftPanel` | 1 | Renders scope description text panel (left side of window) | -| `SharedRenderState` | 1,2,3 | Mutex A — state variables Thread 1 reads each frame to push as shader uniforms | -| `TargetBuffer` | 2,4 | Mutex B — target data handoff between Thread 2 (traffic cop) and Thread 4 (simulator) | -| `TrafficCop` | 2 | Polls Simulator and RPi receivers each beam update; writes targets to SharedRenderState | -| `Simulator` | 4 | Runs fake targets independently; returns data to TrafficCop when polled | -| `KnobPanel` | 3 | Future hardware stub — idles without acquiring Mutex A until Arduino hardware is wired; SharedRenderState holds compile-time defaults from settings.h | -| `RPiReceiver` | 2 | Stub — one instance per Raspberry Pi; called by TrafficCop | - ---- - -## Thread Summary - -| Thread | Class(es) | Mutex Access | -|---|---|---| -| Thread 1 | ScopeManager, all Scope subclasses, PhosphorRenderer, Graticule, LeftPanel | Reads SharedRenderState under Mutex A | -| Thread 2 | TrafficCop, RPiReceiver | Writes SharedRenderState under Mutex A; reads TargetBuffer under Mutex B | -| Thread 3 | KnobPanel | Writes SharedRenderState under Mutex A | -| Thread 4 | Simulator | Writes TargetBuffer under Mutex B | - -Keyboard input arrives via GLFW callback (glfwSetKeyCallback) in Thread 1. -Thread 1 dispatches s/S to ScopeManager and all other keys to the active Scope. - ---- - -## Proposed File Layout - -``` -src/ - main.cpp — GLFW init, thread launch, main loop - scope_manager.h / scope_manager.cpp - scope.h / scope.cpp — abstract Scope base class - scope_intro.h / scope_intro.cpp — ExhibitIntro - scope_ascope.h / scope_ascope.cpp — abstract AScope - scope_marine_a.h / scope_marine_a.cpp - scope_chain_home.h / scope_chain_home.cpp - scope_ppi.h / scope_ppi.cpp — abstract PPIScope - scope_marine_ppi.h / scope_marine_ppi.cpp - scope_atc_ppi.h / scope_atc_ppi.cpp - scope_par.h / scope_par.cpp - phosphor.h / phosphor.cpp - graticule.h / graticule.cpp - left_panel.h / left_panel.cpp - shared_render_state.h / shared_render_state.cpp - target_buffer.h / target_buffer.cpp - traffic_cop.h / traffic_cop.cpp - simulator.h / simulator.cpp - knob_panel.h / knob_panel.cpp - rpi_receiver.h / rpi_receiver.cpp - db_panel.h / db_panel.cpp — Dear ImGui database management panel - (active only when --database flag passed; - no radar rendering in this mode) - settings.h — all tunable constants (no .cpp needed) - - imgui/ — Dear ImGui source, compiled into project - imgui.h / imgui.cpp - imgui_impl_glfw.h / imgui_impl_glfw.cpp - imgui_impl_opengl3.h / imgui_impl_opengl3.cpp - imgui_draw.cpp / imgui_tables.cpp / imgui_widgets.cpp - -shaders/ - phosphor.vert / phosphor.frag — parameterized for P1 and P7 via uniforms - graticule.vert / graticule.frag - text.vert / text.frag - sweep.vert / sweep.frag - bloom.vert / bloom.frag — two-pass bloom: render to FBO, Gaussian - blur bright spots, blend back; used for - target blooming from radar equation output -``` - ---- - -## Robustness and Safety - -### Input Rate Limiting (the mad-child problem) -GLFW key callbacks can fire hundreds of times per second under key-mashing or hardware -encoder noise. Each control variable carries a `last_event_time` timestamp alongside -its value. Any input arriving within `MIN_INPUT_INTERVAL_MS` of the previous accepted -input for that control is silently discarded. The `KEY_MAX_STEP` / `KEY_GONIO_MAX_STEP` -constants cap how far a single accepted input can move a value. Together these make it -impossible to slam a bearing to an extreme in under a second regardless of how fast the -input fires. - -### Control Variable Clamping -Every state variable that a control modifies has companion `MIN_*` and `MAX_*` constants -in `settings.h`. The clamp is applied **at the point of write** using `std::clamp()`, in -every code path: keyboard callback, KnobPanel, and any future source. Thread 1 reads -already-clean values and applies a second `std::clamp()` before passing to the shader as -a second line of defense. No validation logic is needed at the shader boundary. - -### Incoming Data Validation -`RPiReceiver::parseFrame()` and `Simulator::poll()` both return `std::optional`. -A return of `std::nullopt` means the frame was malformed or out of bounds. `TrafficCop` -discards nullopt silently — no exception, no abort, no log spam. Fields validated: -bearing (0–360), range (0–radar max), amplitude (0–1), altitude (0–60,000 ft), timestamp -(not stale, not future), target count (truncated to `MAX_SIMULTANEOUS_TARGETS`), and -frame byte length (rejected above `MAX_RPI_FRAME_BYTES`). - -### Array Bounds Safety -- All fixed-size target storage uses `std::array` — never a raw C array. -- Array views passed between functions use `std::span` (C++20) — size always travels with the pointer. -- Ring buffers for phosphor persistence use modulo indexing (`index % CAPACITY`), never raw pointer arithmetic. -- `static_assert` guards validate that `MAX_TARGETS` and similar constants are within sane limits at compile time. -- Bounds-checked access (`.at()`) used in debug builds; validated index used in release. - -### Thread Safety Rules -1. **Snapshot pattern**: Thread 1 acquires Mutex A, copies the entire `SharedRenderState` struct, releases the lock immediately, then renders from the local copy. No OpenGL calls are ever made while holding a mutex (Core Guideline CP.22). -2. **Always `std::scoped_lock`**: Mutexes are never locked/unlocked manually. `std::scoped_lock` releases on all exit paths including exceptions (SEI CERT CON51). -3. **Lock ordering**: If code ever needs both Mutex A and Mutex B simultaneously, A must be acquired first. Documented here as a project-wide invariant to prevent deadlock (SEI CERT CON53). -4. **Atomic simple flags**: Boolean toggles that are written by one thread and read by another (`prf_high`, `goniometer_mode`) use `std::atomic` — no mutex overhead for single-variable reads. -5. **KnobPanel idle guarantee**: Until hardware is connected, KnobPanel's thread loop sleeps and never calls `lock()` on Mutex A. Zero contention on that mutex from Thread 3. - -### RAII Requirements -Every resource that is opened must be wrapped in an RAII holder so that it closes on any exit path: - -| Resource | RAII mechanism | -|---|---| -| OpenGL VAO / VBO / texture / shader | Thin `GLHandle` wrapper; destructor calls `glDelete*()` | -| GLFW window | `unique_ptr` | -| FreeType `FT_Library` / `FT_Face` | Small RAII structs; destructor calls `FT_Done_*` | -| Mutex locks | `std::scoped_lock` or `std::lock_guard` — never bare lock/unlock | -| Worker threads | `std::jthread` (C++20) — auto-joins on destruction; no detached threads | -| File handles (shader source) | `std::ifstream` — RAII by default | - -No raw `new` or `delete` anywhere in the codebase. No `malloc`/`free`. - -### SEI CERT C++ Compliance — Key Rules -| Rule | Enforcement | -|---|---| -| CON50 | Never destroy a mutex while locked — `std::scoped_lock` makes this structurally impossible | -| CON51 | Release locks on exceptions — `std::scoped_lock` handles automatically | -| CON53 | Consistent lock ordering (A before B) — documented invariant | -| ARR50 | Array indices always validated before use or bounded by `std::array::at()` | -| MEM51 | All resources managed by RAII wrappers; no raw `new`/`delete` | -| ERR50 | No `abort()` or `exit()` in normal operation; bad data is dropped, not fatal | -| INT30 | Bearing arithmetic uses `fmod()`, not unsigned wraparound subtraction | -| FLP30 | No floating-point loop counters; sweep angle is a double accumulator | -| OOP50 | No virtual method calls in constructors or destructors | - -### C++ Core Guidelines Compliance — Key Rules -| Guideline | Enforcement | -|---|---| -| I.6 / I.7 | `Expects()` / `Ensures()` (GSL) at function entry/exit in debug builds | -| I.12 | `gsl::not_null` for the active scope pointer in ScopeManager | -| R.1 | All resource management through RAII handles (see table above) | -| CP.20 | `std::scoped_lock` for all mutex acquisition — no bare lock/unlock | -| CP.21 | `std::scoped_lock(mutexA, mutexB)` if both must be held simultaneously | -| CP.22 | No OpenGL calls while holding any mutex — snapshot pattern enforces this | -| Bounds.1 | No pointer arithmetic; `std::span` for array views | -| Bounds.2 | Array access only via `.at()` or pre-validated index | -| Bounds.4 | No `sprintf`, `strcpy`, `gets`; use `std::string` and `std::format` (C++20) | - ---- - -## Key Design Notes - -1. **ScopeManager** sits in Thread 1 and holds the active scope pointer. The GLFW - key callback calls ScopeManager::handleKey(), which dispatches s/S to itself - and all other keys to the active scope. The auto-advance timer resets on any - key event or control input. - -2. **SharedRenderState** holds two categories: control state (bearing, range, - cursor — written by Thread 1 keyboard callbacks and Thread 3 knob panel) and - target state (written by Thread 2). Thread 1 reads the whole struct once per - frame under Mutex A to push uniforms to the shaders. - -3. **TargetBuffer** is separate from SharedRenderState — it is the handoff point - between Thread 2 (traffic cop) and Thread 4 (simulator) under Mutex B. - -4. **PhosphorRenderer** is a shared utility. AScope and PPIScope subclasses - receive it as a constructor dependency rather than each reimplementing decay - logic. Pass phosphor type (P1/P7) and decay constant as parameters. - -5. **Shaders** are parameterized via uniforms rather than duplicated. A single - phosphor shader pair handles both P1 (green, no persistence) and P7 (blue - strike, yellow-green decay) by passing color and decay time as uniforms. - -6. **Shoreline and terrain geometry** is loaded once at Thread 1 startup as a - static VBO. It is read-only after load — no mutex needed. - -7. **Each scope's max range and cursor state are independent.** Changing range - on the Marine PPI does not affect the ATC PPI, and vice versa. State is - owned by each scope instance. - -8. **PAR sub-scopes** (azimuth and elevation) can be implemented as private - member objects within PARScope rather than as separate Scope subclasses, - since they are never displayed independently. - -9. **settings.h** is a header-only file (no .cpp). It will contain only - `constexpr` constants organized into named sections. Every other source - file that needs a tunable value includes this file. No magic numbers - anywhere else in the codebase. Add candidate variables here before - coding begins; values can be refined during debugging and appearance work. - Sections defined: phosphor colors (P1/P7), sweep parameters, graticule - geometry and colors, cursor, noise floor, graticule swap animation timing, - key-hold acceleration, auto-advance timer, window/layout geometry, text - colors/sizes, **general operator control defaults** (Intensity, Focus, - Astigmatism, Gain, Rain Clutter, Wave Clutter, Graticule Intensity), - **per-scope default state** (initial bearing, range, cursor, calibrator - scale for each scope), and **PAR geometry** (non-linear scale breakpoint, - runway heading, glide path angle, scan widths). - -10. **General operator controls** (Intensity, Focus, Astigmatism, Gain, Rain - Clutter, Wave Clutter, Graticule Intensity) are placeholders for physical - encoders not yet purchased. Each has a `DEFAULT_*` constant in settings.h - and a corresponding variable in `SharedRenderState`. KnobPanel (Thread 3) - has a stub write path for each. The thread starts and idles, but never - calls `lock()` on Mutex A, so it imposes zero contention. When hardware - arrives, only KnobPanel changes — Thread 1 and the shaders are already - fully wired to consume the values. - -11. **MarineAScope graticule swap** is a state machine with four states: - NORMAL → SLIDING_OUT → BARE_CRT → SLIDING_IN → NORMAL. The u and d keys - are blocked during the animation. The new range value is latched when the - key is pressed but not applied to the scope state until SLIDING_IN completes. - Animation duration is approximately 0.5 seconds per slide (out and in). - -12. **All dimensions are stored and computed in meters** throughout the system. - Any incoming data from Raspberry Pi receivers or the simulator that arrives in - feet (e.g., altitude from ADS-B in feet, antenna heights) is converted to - meters at the boundary in `RPiReceiver::parseFrame()` or `Simulator::poll()` - before the value enters any shared data structure. No feet values appear - anywhere inside the system after the conversion point. - Conversion: 1 foot = 0.3048 meters exactly. - -13. **Default target dimensions** used when a target is first seen with no - database record. All values in meters. `need_update` is set TRUE for all - defaults so the operator knows to fill in real data. - - | Category | Length (m) | Fuselage/Beam width (m) | Material | - |---|---|---|---| - | GA aircraft | 4.0 | 1.0 | aluminum | - | Commercial a/c| 30.0 | 5.0 | aluminum | - | AIS vessel | 20.0 | 5.0 | steel | - | Simulator boat| 6.0 | 2.0 | fiberglass | - - AIS-sourced vessels default to steel (legally required commercial traffic). - Simulator-sourced boats default to fiberglass (small pleasure craft). - Aircraft source type does not disambiguate GA vs. commercial — the system - defaults to GA and lets the operator correct it. - -14. **Bloom post-processing** uses a dedicated `bloom.vert` / `bloom.frag` shader - pair. The pipeline is: render targets to an offscreen FBO at full computed - brightness (from radar equation output), apply a two-pass Gaussian blur to - pixels above a luminance threshold, then additively blend the blurred result - back onto the main framebuffer. Bloom threshold and blur radius are - `constexpr` constants in `settings.h`. - -15. **Dear ImGui** is used for the database management panel, activated by the - `--database` command-line flag. In that mode no radar rendering occurs — - main.cpp skips the scope/shader initialization path entirely and starts the - ImGui loop instead. ImGui source files live under `src/imgui/` and are - compiled directly into the project (no separate install step). The panel - provides: a scrollable target table with `need_update` highlighted, inline - edit fields for length/width/height/material, a dropdown for target type, - and a Save button that writes to PostgreSQL via libpq. - -16. **Chain Home RCS resonance** is modelled with a multiplier constant - `CHAIN_HOME_RCS_RESONANCE_FACTOR` in `settings.h`. At 30 MHz (λ ≈ 10 m), - aircraft with wingspans of 10–30 m are in the Mie/resonant scattering - region; RCS can be 2–5× the geometric cross section. The default value is - 3.0 (a mid-range estimate). This is applied in the radar equation computation - for Chain Home targets only, before the result is passed to the bloom/ - brightness pipeline. - -17. **Terrain and land clutter** are rendered from pre-processed binary grids - in `map/lidar_processed/`. The offline tool `terrain_preprocess` fuses the - SRTM DEM, both LiDAR surveys, and the S-57 ENC into elevation, material, - and shadow-mask grids. At runtime `TerrainMap` loads these grids once; - `LandClutter` generates a polar clutter texture once per sweep period. - See the TERRAIN CLUTTER VISUAL DESIGN REFERENCE section below for - per-material appearance and speckle tuning guidance. - ---- - -## Terrain Clutter Visual Design Reference - -All tunable values described here have corresponding constants in -`src/settings.h`. Edit `settings.h` to change values; this section -explains the perceptual intent behind each constant. - -### Purpose - -The goal is historical authenticity. Period marine radar operators in the -1950s saw the Bellingham shoreline as a bright, stable ring of returns that -formed a recognizable coastline silhouette. Skilled operators mentally -subtracted this from the display and watched only for moving or changing -returns. That experience should be reproducible on this exhibit. - ---- - -### Material Visual Character - -**SOIL** (vegetated land, fields, low hills) - -Period appearance: moderate-brightness, moderately grainy returns. The -grain (speckle) is high because vegetation is irregular — individual trees, -bushes, and undulations produce slightly different returns each sweep. -The overall brightness holds steady between sweeps but the texture shimmers. - -- σ° constant: `TERRAIN_SIGMA0_SOIL` (~0.010, −20 dB) -- Speckle: `TERRAIN_SPECKLE_SOIL` (suggested starting value: **0.35**) -- Appearance: mid-grey; clearly visible but not dominating. Rolling - hillsides read as a diffuse bright edge along the coastline - and inland ridges. - ---- - -**ROCK** (exposed cliff faces, upper Chuckanut ridgeline, rock outcrops) - -Period appearance: brighter than soil, less speckle. Rock faces are -geometrically consistent so returns are stable sweep to sweep. Steep faces -pointing toward the radar return a disproportionately strong echo. - -- σ° constant: `TERRAIN_SIGMA0_ROCK` (~0.032, −15 dB) -- Speckle: `TERRAIN_SPECKLE_ROCK` (suggested starting value: **0.20**) -- Appearance: noticeably brighter than soil; Chuckanut Mountain's western - face reads as a bright arc, stable between sweeps. Little - shimmer — the texture is coarser and more consistent. - ---- - -**CONCRETE** (breakwaters, piers, dock structures, Boulevard Park boardwalk, -harbor facilities) - -Period appearance: the strongest land returns on the scope after large steel -vessels. Structures built over water (piers on pilings, breakwater walls) -produce corner-reflector effects — the right-angle junction between the -vertical face and the water surface acts as a retroreflector. Operators used -these as navigation aids; the Bellingham harbor entrance is identifiable by -its distinctive bright return pattern. - -- σ° constant: `TERRAIN_SIGMA0_CONCRETE` (~0.100, −10 dB) -- Speckle: `TERRAIN_SPECKLE_CONCRETE` (suggested starting value: **0.12**) -- Appearance: bright, stable, low shimmer. Breakwaters and piers appear as - sharp bright lines or arcs. The Boulevard Park boardwalk over - the water appears as a bright thin arc. These features should - be the most prominent land returns on the scope after large - steel ships. - ---- - -**WATER — CALM** (open bay, light wind) - -Period appearance: very weak, near-invisible. Calm water reflects most radar -energy away from the antenna (specular reflection). Operators saw a nearly -blank area over open water even at high gain. - -- σ° constant: `TERRAIN_SIGMA0_WATER_CALM` (~0.0003, −35 dB) -- Appearance: at normal gain, essentially black. Only visible at extreme - gain settings as a faint salt-and-pepper noise floor. - ---- - -**WATER — ROUGH** (choppy bay, wind >10 knots) - -Period appearance: a low-level fuzzy return rising from the noise floor, -affecting the inner ranges most strongly. Sea clutter was a major nuisance -on small-vessel marine radar. The wave clutter filter (keys 5/6) suppresses -this. - -- σ° constant: `TERRAIN_SIGMA0_WATER_ROUGH` (~0.010, −20 dB) -- Appearance: at normal gain, a hazy shimmer at short ranges that fades - outward. Heavy speckle — random wave facets scatter - incoherently. The wave clutter filter reduces this. - ---- - -### Speckle / Grain Tuning Guide - -Speckle simulates pulse-to-pulse amplitude variation caused by incoherent -scattering from irregular surfaces. Implemented as a per-cell random -fraction multiplied by the computed P_r each sweep: - -``` -Speckle = P_r × (1.0 + TERRAIN_SPECKLE_xxx × random(−1, +1)) -``` - -Values close to 0.0 give stable, solid returns (correct for concrete and -large flat surfaces). Values closer to 0.5 give vigorous shimmer (correct -for vegetation and choppy water). Values above 0.5 are unrealistically noisy -for terrain — avoid unless simulating a very rough or complex surface. - -Suggested starting values: - -| Constant | Value | Character | -|-----------------------------|-------|-----------------------------------------| -| `TERRAIN_SPECKLE_SOIL` | 0.35 | visible shimmer, naturalistic | -| `TERRAIN_SPECKLE_ROCK` | 0.20 | moderate, stable with some texture | -| `TERRAIN_SPECKLE_CONCRETE` | 0.12 | mostly stable; slight flicker from edge | - -**Tuning procedure:** -1. Set max range to 6 miles on the Marine PPI. -2. Rotate to a bearing showing the Bellingham breakwater and open bay. -3. Adjust `TERRAIN_MARINE_CLUTTER_BRIGHTNESS` until the breakwater return - is clearly bright but does not wash out nearby ship targets. -4. Adjust `TERRAIN_SIGMA0_CONCRETE` if breakwater brightness relative to - soil hills looks wrong. -5. Adjust speckle values until soil hillsides shimmer naturally and concrete - structures hold steady between sweeps. -6. Verify at all three marine range settings (2, 4, 6 miles) that clutter - does not overwhelm vessel targets at any range. - ---- - -### Overall Brightness Balance - -The most important perceptual tuning parameter is -`TERRAIN_MARINE_CLUTTER_BRIGHTNESS`. It is a linear scale factor applied to -all terrain return brightness before the clutter texture is uploaded to the -GPU. It does not change the relative balance between materials — it scales -all terrain uniformly. - -**Target appearance goal:** -- A steel AIS vessel at 3 miles range should be 2–3× brighter than the - strongest adjacent land clutter return (a concrete breakwater). -- The coastline silhouette should be clearly readable as geography — a - visitor who knows Bellingham Bay should recognize the shape. -- Open water should be visually clean at default gain settings. - -Suggested starting value: `TERRAIN_MARINE_CLUTTER_BRIGHTNESS = 0.55` - -**ATC PPI:** `ATC_TERRAIN_CLUTTER_SUPPRESSED = true` means land clutter is -hidden by MTI cancellation — no brightness tuning needed for ATC. If -suppression is disabled for debugging, use `TERRAIN_MARINE_CLUTTER_BRIGHTNESS` -as a guide. - ---- - -### Shadow Zones - -Shadow zones are the most dramatic visual effect on the Marine PPI. They -appear as dark wedge-shaped gaps in the land clutter pattern, pointing -outward from the radar, behind ridgelines and hills. - -Key shadow features visible from the marine bay platform: -- **Chuckanut Mountain** (southwest) casts a prominent shadow into the - southern bay and beyond. -- **Lummi Island** (northwest) shadows a sector of the northern bay. -- **Bellingham waterfront bluff** shadows part of the inner harbor. - -These shadows are inherently dark against surrounding clutter — no tuning -required. Shadow = zero amplitude, computed geometrically by the preprocessor. - -For the ATC scope with `ATC_TERRAIN_SHADOW_ENABLED = true`, shadows affect -aircraft returns only (not the display background, which is suppressed). An -aircraft descending behind Chuckanut Mountain will fade out and reappear as -it clears the ridge on final approach — authentic behavior of period ASR radar. - ---- - -### Polar Clutter Texture Resolution - -`TERRAIN_POLAR_BEARING_BINS` and `TERRAIN_POLAR_RANGE_BINS` control the -resolution of the GPU texture carrying terrain clutter to the shader. -Recommended values: - -| Scope | Bearing bins | Range bins | Notes | -|---------------------|-------------|------------|------------------------------| -| Marine PPI (6 mi) | 720 | 512 | 0.5° matches marine beamwidth| -| ATC PPI (20 mi) | 720 | 1024 | wider range needs more bins | - -The texture is regenerated once per sweep period (every 4–5 seconds), not -every frame. Upload cost is not time-critical. Reducing to 360 bearing bins -is acceptable and halves the texture upload cost at the expense of slightly -blocky angular transitions near prominent features.