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