clean out test files
This commit is contained in:
107
CMakeLists.txt
107
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)
|
||||
|
||||
606
DESIGN.md
606
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<TargetData>`.
|
||||
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<TargetData, MAX_TARGETS>` — never a raw C array.
|
||||
- Array views passed between functions use `std::span<TargetData>` (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<bool>` — 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<T>` wrapper; destructor calls `glDelete*()` |
|
||||
| GLFW window | `unique_ptr<GLFWwindow, decltype(&glfwDestroyWindow)>` |
|
||||
| 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<Scope*>` 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.
|
||||
|
||||
Reference in New Issue
Block a user