This commit is contained in:
2026-06-17 10:35:08 -07:00
parent e3b09170a4
commit 5355a9eded
9 changed files with 1836 additions and 0 deletions

38
CMakeLists.txt Normal file
View File

@@ -0,0 +1,38 @@
cmake_minimum_required(VERSION 3.16)
project(radar_simulator)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(OpenGL REQUIRED)
find_package(glfw3 REQUIRED)
find_package(Freetype REQUIRED)
find_package(Threads REQUIRED)
# GLAD is bundled in the project tree
add_library(glad STATIC
${CMAKE_SOURCE_DIR}/glad/src/glad.c
)
target_include_directories(glad PUBLIC
${CMAKE_SOURCE_DIR}/include
)
add_executable(radar_simulator
${CMAKE_SOURCE_DIR}/global_data.cpp
${CMAKE_SOURCE_DIR}/src/main.cpp
${CMAKE_SOURCE_DIR}/src/01_classes.cpp
${CMAKE_SOURCE_DIR}/src/02_text_description.cpp
)
target_include_directories(radar_simulator PRIVATE
${CMAKE_SOURCE_DIR}/include
${FREETYPE_INCLUDE_DIRS}
)
target_link_libraries(radar_simulator PRIVATE
glad
OpenGL::GL
glfw
${FREETYPE_LIBRARIES}
Threads::Threads
)

92
include/01_classes.h Normal file
View File

@@ -0,0 +1,92 @@
/*
* MIT License
*
* Copyright (c) 2026 Mark Allyn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* 01_classes.h
* Author: Mark Allyn
* Component: 01_classes
*
* Abstract RadarBase class and four stub subclasses.
* Each stub renders its name and description in the three screen panels.
* No real radar simulation is performed here; this is for testing
* the initialization and radar-management/selection flow.
*/
#pragma once
#include "data_structure_and_constants.h"
#include "settings.h"
/* Abstract base — one instance per radar type. */
class RadarBase {
public:
/* Render radar description and control list into the left panel viewport. */
virtual void render_left_panel() = 0;
/* Render scope content (stub: radar name only) into the scope viewport. */
virtual void render_scope() = 0;
/* Render current control values into the status bar viewport. */
virtual void render_status_bar() = 0;
virtual ~RadarBase() = default;
};
/* Chain Home radar (1940s A-scope) — Ventnor, Isle of Wight. */
class ChainHomeRadar : public RadarBase {
public:
void render_left_panel() override;
void render_scope() override;
void render_status_bar() override;
};
/* Marine A-Scope radar — Community Boating Center, Bellingham Bay. */
class MarineAScopeRadar : public RadarBase {
public:
void render_left_panel() override;
void render_scope() override;
void render_status_bar() override;
};
/* Marine Traffic Control PPI radar — center of Bellingham Bay. */
class MarineTrafficRadar : public RadarBase {
public:
void render_left_panel() override;
void render_scope() override;
void render_status_bar() override;
};
/* Police Boat PPI radar — Taylor Dock / Boulevard Park, Bellingham Bay. */
class PoliceboatRadar : public RadarBase {
public:
void render_left_panel() override;
void render_scope() override;
void render_status_bar() override;
};
/* One global instance per radar — indexed by RADAR_* constants in main.cpp. */
extern ChainHomeRadar g_chain_home;
extern MarineAScopeRadar g_marine_ascope;
extern MarineTrafficRadar g_marine_traffic;
extern PoliceboatRadar g_police_boat;

View File

@@ -0,0 +1,96 @@
/*
* MIT License
*
* Copyright (c) 2026 Mark Allyn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* 02_text_description.h
* Author: Mark Allyn
* Component: 02_text_description
*
* FreeType-based text renderer used by all three screen panels
* (left description panel, scope panel, status bar).
*
* Usage:
* 1. Call g_text_renderer.initialize_shaders() after GLAD is loaded.
* 2. Call g_text_renderer.initialize_font(FONT_PATH, FONT_SIZE_NORMAL).
* 3. Before rendering into a viewport, call set_projection(w, h).
* 4. Call render_text() with viewport-local pixel coordinates.
*/
#pragma once
#include <string>
#include <map>
#include <glad/glad.h>
#include "data_structure_and_constants.h"
#include "settings.h"
/* Cached metrics for one loaded glyph. */
struct glyph_character {
GLuint texture_id; // GL_RED single-channel texture
int size_x; // bitmap width in pixels
int size_y; // bitmap height in pixels
int bearing_x; // pixel offset from cursor to left of glyph
int bearing_y; // pixel offset from cursor baseline to top of glyph
unsigned int advance; // horizontal advance in 1/64-pixel units
};
class TextRenderer {
public:
TextRenderer() : vao_(0), vbo_(0), shader_program_(0) {}
/* Compile and link 02_text_description_{vert,frag}.glsl.
* Must be called once, after GLAD is initialised. */
bool initialize_shaders();
/* Load glyphs for printable ASCII (32126) from the given font file.
* font_size is the desired pixel height. */
bool initialize_font(const std::string& font_path, unsigned int font_size);
/* Render text at (x, y) in viewport-local pixel coordinates.
* y=0 is the bottom of the current viewport.
* scale multiplies the loaded glyph size.
* color_r/g/b are 0.01.0. */
void render_text(const std::string& text,
float x, float y, float scale,
float color_r, float color_g, float color_b);
/* Return the rendered pixel width of text at the given scale. */
float get_text_width(const std::string& text, float scale) const;
/* Set the orthographic projection for a viewport of w × h pixels.
* Call this each time glViewport changes before rendering text. */
void set_projection(int w, int h);
/* Release all GL resources and glyph textures. */
void cleanup();
private:
std::map<char, glyph_character> characters_;
GLuint vao_;
GLuint vbo_;
GLuint shader_program_;
};
/* Global instance — shared by all panels and radar stubs. */
extern TextRenderer g_text_renderer;

View File

@@ -126,3 +126,79 @@
#define PPI_BOAT_RANGE_STEP_3 6.0f #define PPI_BOAT_RANGE_STEP_3 6.0f
#define PPI_BOAT_RANGE_STEP_4 12.0f #define PPI_BOAT_RANGE_STEP_4 12.0f
#define PPI_BOAT_RANGE_STEP_5 24.0f #define PPI_BOAT_RANGE_STEP_5 24.0f
/* ============================================================
* WINDOW AND PANEL LAYOUT
* Component: Thread 1 (main.cpp), 01_classes, 02_text_description
* ============================================================ */
#define WINDOW_WIDTH 1920
#define WINDOW_HEIGHT 1080
#define WINDOW_TITLE "RAF / Marine Radar Exhibit"
// Left text panel occupies left side; scope and status bar share the right side
#define LEFT_PANEL_WIDTH 500
#define STATUS_BAR_HEIGHT 250
// Derived dimensions — scope area fills remaining right-side space
#define SCOPE_WIDTH (WINDOW_WIDTH - LEFT_PANEL_WIDTH)
#define SCOPE_HEIGHT (WINDOW_HEIGHT - STATUS_BAR_HEIGHT)
/* ============================================================
* TEXT RENDERING
* Component: 02_text_description
* ============================================================ */
#define FONT_PATH "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
#define FONT_SIZE_NORMAL 18u
#define FONT_SIZE_TITLE 24u
// Pixels between text baselines at scale 1.0 with FONT_SIZE_NORMAL
#define LINE_HEIGHT 24.0f
// Pixel margin from panel edge for text placement
#define TEXT_MARGIN 10.0f
/* ============================================================
* SHADER DIRECTORY
* Component: all components that load shaders
* ============================================================ */
// Path is relative to the build/ directory where the executable lives
#define SHADER_DIR "../shaders/"
/* ============================================================
* KEYBOARD CONTROL STEP SIZES
* Component: Thread 1 keyboard handler (main.cpp)
* ============================================================ */
#define INTENSITY_STEP 0.05f
#define SENSITIVITY_STEP 0.05f
#define STC_SENSITIVITY_STEP 0.05f
#define STC_RANGE_STEP 0.05f
#define NOISE_FILTER_STEP 0.05f
#define RADIOGON_STEP 1.0f
#define BEARING_CURSOR_STEP 1.0f
#define RANGE_CURSOR_STEP 0.02f
#define MARINE_A_BEARING_STEP 1.0f
#define MAX_RANGE_STEP 0.1f
/* ============================================================
* KEYBOARD CONTROL LIMITS
* Component: Thread 1 keyboard handler (main.cpp)
* ============================================================ */
#define INTENSITY_MIN 0.0f
#define INTENSITY_MAX 2.0f
#define SENSITIVITY_MIN 0.0f
#define SENSITIVITY_MAX 2.0f
#define STC_SENSITIVITY_MIN 0.0f
#define STC_SENSITIVITY_MAX 1.0f
#define STC_RANGE_MIN 0.0f
#define STC_RANGE_MAX 1.0f
#define NOISE_FILTER_MIN 0.0f
#define NOISE_FILTER_MAX 1.0f
#define RANGE_CURSOR_MIN 0.0f
#define RANGE_CURSOR_MAX 1.0f
#define MAX_RANGE_MIN 0.1f
#define MAX_RANGE_MAX 1.0f

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2026 Mark Allyn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* 02_text_description_frag.glsl
* Author: Mark Allyn
* Component: 02_text_description
*
* Fragment shader for FreeType glyph rendering.
* Glyphs are uploaded as GL_RED single-channel textures; the red channel
* drives the alpha so the background shows through unfilled pixels.
*/
#version 430 core
in vec2 tex_coords;
out vec4 frag_color;
uniform sampler2D text_sampler; // bound to texture unit 0
uniform vec3 text_color; // RGB, 0.01.0
void main() {
float alpha = texture(text_sampler, tex_coords).r;
frag_color = vec4(text_color, alpha);
}

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2026 Mark Allyn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* 02_text_description_vert.glsl
* Author: Mark Allyn
* Component: 02_text_description
*
* Vertex shader for FreeType glyph quad rendering.
* Each vertex carries both its screen position and its texture coordinate
* packed into a single vec4 to minimise attribute slots.
*/
#version 430 core
layout (location = 0) in vec4 vertex; // xy = screen position (pixels), zw = texture coords
out vec2 tex_coords;
uniform mat4 projection; // orthographic: maps pixel space to NDC for current viewport
void main() {
gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
tex_coords = vertex.zw;
}

453
src/01_classes.cpp Normal file
View File

@@ -0,0 +1,453 @@
/*
* MIT License
*
* Copyright (c) 2026 Mark Allyn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* 01_classes.cpp
* Author: Mark Allyn
* Component: 01_classes
*
* Stub implementations for all four radar subclasses.
* Each renders description text in the left panel, the radar name in the
* scope panel, and current control values in the status bar.
* No real radar simulation occurs here; this exists to test the
* initialization and radar-management/selection flow.
*/
#include "01_classes.h"
#include "02_text_description.h"
#include "data_structure_and_constants.h"
#include "settings.h"
#include <cstdio>
#include <string>
/* ------------------------------------------------------------------ */
/* Global instances */
/* ------------------------------------------------------------------ */
ChainHomeRadar g_chain_home;
MarineAScopeRadar g_marine_ascope;
MarineTrafficRadar g_marine_traffic;
PoliceboatRadar g_police_boat;
/* ------------------------------------------------------------------ */
/* Colour constants (RGB, 0.01.0) */
/* ------------------------------------------------------------------ */
static const float COL_WHITE[3] = { 1.00f, 1.00f, 1.00f };
static const float COL_RED[3] = { 1.00f, 0.25f, 0.25f };
static const float COL_PINK[3] = { 1.00f, 0.60f, 0.80f };
static const float COL_YELLOW[3] = { 1.00f, 1.00f, 0.00f };
static const float COL_GREEN_DIM[3] = { 0.00f, 0.85f, 0.30f };
static const float COL_GREEN_LO[3] = { 0.00f, 0.55f, 0.18f };
/* ------------------------------------------------------------------ */
/* Layout helpers */
/* ------------------------------------------------------------------ */
static const float SCALE_TITLE = 1.25f;
static const float SCALE_NORMAL = 1.00f;
static const float SCALE_SMALL = 0.85f;
static const float MARGIN = static_cast<float>(TEXT_MARGIN);
static const float LH = LINE_HEIGHT; /* line height at scale 1.0 */
/* Render one line of text and advance y downward by one line. */
static void text_line(const std::string& text, float x, float& y,
float scale,
float cr, float cg, float cb) {
g_text_renderer.render_text(text, x, y, scale, cr, cg, cb);
y -= LH * scale;
}
/* Render a blank spacer line. */
static void blank_line(float& y, float scale) {
y -= LH * scale * 0.6f;
}
/* Render one control entry: label in red, keys in pink, on the same line.
* Format: " label_text key_lo / key_hi"
* Returns the new y after advancing one line. */
static void control_line(const std::string& label,
const std::string& key_lo, const std::string& key_hi,
float x, float& y, float scale) {
float xp = x + 6.0f;
/* Label in red */
g_text_renderer.render_text(label, xp, y, scale,
COL_RED[0], COL_RED[1], COL_RED[2]);
xp += g_text_renderer.get_text_width(label, scale);
/* Lower key in pink */
g_text_renderer.render_text(key_lo, xp, y, scale,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width(key_lo, scale);
/* Separator in red */
g_text_renderer.render_text(" / ", xp, y, scale,
COL_RED[0], COL_RED[1], COL_RED[2]);
xp += g_text_renderer.get_text_width(" / ", scale);
/* Upper key in pink */
g_text_renderer.render_text(key_hi, xp, y, scale,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
y -= LH * scale;
}
/* ------------------------------------------------------------------ */
/* Scope helper: render centred name + stub subtitle */
/* ------------------------------------------------------------------ */
static void render_scope_name(const std::string& radar_name) {
const float scope_w = static_cast<float>(SCOPE_WIDTH);
const float scope_h = static_cast<float>(SCOPE_HEIGHT);
float name_w = g_text_renderer.get_text_width(radar_name, 2.0f);
float nx = (scope_w - name_w) / 2.0f;
float ny = scope_h / 2.0f + 20.0f;
g_text_renderer.render_text(radar_name, nx, ny, 2.0f,
COL_GREEN_DIM[0], COL_GREEN_DIM[1], COL_GREEN_DIM[2]);
std::string sub = "[ stub - initialization test only ]";
float sub_w = g_text_renderer.get_text_width(sub, SCALE_NORMAL);
float sx = (scope_w - sub_w) / 2.0f;
float sy = ny - LH * 2.0f;
g_text_renderer.render_text(sub, sx, sy, SCALE_NORMAL,
COL_GREEN_LO[0], COL_GREEN_LO[1], COL_GREEN_LO[2]);
}
/* ------------------------------------------------------------------ */
/* Status bar helper: common controls (intensity, sensitivity, STC) */
/* ------------------------------------------------------------------ */
static void render_common_status(float x, float& y, float scale) {
char buf[320];
snprintf(buf, sizeof(buf),
"Intensity: %.2f | Sensitivity: %.2f | STC Sens: %.2f | STC Range: %.2f",
uniform_intensity, uniform_sensitivity,
uniform_stc_sensitivity, uniform_stc_range);
g_text_renderer.render_text(buf, x, y, scale,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
y -= LH * scale;
}
/* ================================================================== */
/* ChainHomeRadar */
/* ================================================================== */
void ChainHomeRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float x = MARGIN;
text_line("CHAIN HOME RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Location: Ventnor, Isle of Wight", x, y, SCALE_SMALL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("The UK developed Chain Home in the 1940s", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("to detect German aircraft attacking across", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("the English Channel. It was one of the", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("first operational radar systems ever built.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("The scope shows range on the horizontal", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("axis and echo amplitude on the vertical.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Bearing is found with the radiogoniometer:", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("rotate the search coil until the echo", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("amplitude reaches a minimum (null point).", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
/* Controls header */
text_line("--- CONTROLS ---", x, y, SCALE_NORMAL,
COL_RED[0], COL_RED[1], COL_RED[2]);
control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL);
control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL);
control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL);
control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL);
control_line("Radiogoniometer: ", "U", "I", x, y, SCALE_NORMAL);
blank_line(y, SCALE_NORMAL);
text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
void ChainHomeRadar::render_scope() {
render_scope_name("CHAIN HOME RADAR");
}
void ChainHomeRadar::render_status_bar() {
float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
text_line("CHAIN HOME RADAR (Ventnor, Isle of Wight) | Fixed range: 200 km",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[200];
snprintf(buf, sizeof(buf), "Radiogoniometer: %.1f deg", uniform_radiogoniometer);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
/* ================================================================== */
/* MarineAScopeRadar */
/* ================================================================== */
void MarineAScopeRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float x = MARGIN;
text_line("MARINE A-SCOPE RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Location: Community Boating Center,", x, y, SCALE_SMALL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("Bellingham Bay, Washington", x, y, SCALE_SMALL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Microwave radar was developed after", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("Chain Home. Its shorter wavelengths", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("could detect small targets such as a", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("submarine periscope or conning tower.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("A steerable dish is connected to an", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("oscilloscope. Bearing is determined by", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("rotating the dish with the servo control.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("--- CONTROLS ---", x, y, SCALE_NORMAL,
COL_RED[0], COL_RED[1], COL_RED[2]);
control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL);
control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL);
control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL);
control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL);
control_line("Max Range: ", "O", "P", x, y, SCALE_NORMAL);
control_line("Antenna Rotation: ", "G", "H", x, y, SCALE_NORMAL);
blank_line(y, SCALE_NORMAL);
text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
void MarineAScopeRadar::render_scope() {
render_scope_name("MARINE A-SCOPE RADAR");
}
void MarineAScopeRadar::render_status_bar() {
float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
text_line("MARINE A-SCOPE RADAR (Community Boating Center, Bellingham Bay)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[200];
snprintf(buf, sizeof(buf),
"Max Range (normalized): %.2f | Antenna Bearing: %.1f deg",
uniform_max_range, uniform_marine_a_bearing);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
/* ================================================================== */
/* MarineTrafficRadar */
/* ================================================================== */
void MarineTrafficRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float x = MARGIN;
text_line("MARINE TRAFFIC CONTROL RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Location: Center of Bellingham Bay,", x, y, SCALE_SMALL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("Washington", x, y, SCALE_SMALL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("The Plan Position Indicator (PPI) radar", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("sweeps a beam clockwise from north.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("Range is the distance of a blip from", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("centre; bearing is the beam direction.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("All modern marine radars use this type", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("of display. Coastal guard stations use", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("them to manage harbour traffic.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("--- CONTROLS ---", x, y, SCALE_NORMAL,
COL_RED[0], COL_RED[1], COL_RED[2]);
control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL);
control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL);
control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL);
control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL);
control_line("Rain Noise Filter: ", "T", "Y", x, y, SCALE_NORMAL);
control_line("Max Range: ", "O", "P", x, y, SCALE_NORMAL);
control_line("Range Cursor: ", "A", "S", x, y, SCALE_NORMAL);
control_line("Bearing Cursor: ", "D", "F", x, y, SCALE_NORMAL);
blank_line(y, SCALE_NORMAL);
text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
void MarineTrafficRadar::render_scope() {
render_scope_name("MARINE TRAFFIC CONTROL RADAR");
}
void MarineTrafficRadar::render_status_bar() {
float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
text_line("MARINE TRAFFIC CONTROL RADAR (Bellingham Bay)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[280];
snprintf(buf, sizeof(buf),
"Noise Filter: %.2f | Max Range (norm): %.2f | "
"Range Cursor: %.2f | Bearing Cursor: %.1f deg",
uniform_noise_filter, uniform_max_range,
uniform_range_cursor, uniform_bearing_cursor);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
/* ================================================================== */
/* PoliceboatRadar */
/* ================================================================== */
void PoliceboatRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float x = MARGIN;
text_line("POLICE BOAT RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Starting location: Taylor Dock /", x, y, SCALE_SMALL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("Boulevard Park, Bellingham Bay", x, y, SCALE_SMALL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("This PPI radar is mounted on a moving", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("police boat patrolling Bellingham Bay.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("The sweep rotates clockwise relative to", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("the boat's heading, not true north. A", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("small indicator shows where north is.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Watch how the display changes as the", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line("boat moves and turns.", x, y, SCALE_NORMAL,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("--- CONTROLS ---", x, y, SCALE_NORMAL,
COL_RED[0], COL_RED[1], COL_RED[2]);
control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL);
control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL);
control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL);
control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL);
control_line("Rain Noise Filter: ", "T", "Y", x, y, SCALE_NORMAL);
control_line("Max Range: ", "O", "P", x, y, SCALE_NORMAL);
control_line("Range Cursor: ", "A", "S", x, y, SCALE_NORMAL);
control_line("Bearing Cursor: ", "D", "F", x, y, SCALE_NORMAL);
blank_line(y, SCALE_NORMAL);
text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
void PoliceboatRadar::render_scope() {
render_scope_name("POLICE BOAT RADAR");
}
void PoliceboatRadar::render_status_bar() {
float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
text_line("POLICE BOAT RADAR (Bellingham Bay - moving)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[280];
snprintf(buf, sizeof(buf),
"Noise Filter: %.2f | Max Range (norm): %.2f | "
"Range Cursor: %.2f | Bearing Cursor: %.1f deg",
uniform_noise_filter, uniform_max_range,
uniform_range_cursor, uniform_bearing_cursor);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}

278
src/02_text_description.cpp Normal file
View File

@@ -0,0 +1,278 @@
/*
* MIT License
*
* Copyright (c) 2026 Mark Allyn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* 02_text_description.cpp
* Author: Mark Allyn
* Component: 02_text_description
*
* FreeType-based text renderer. Loads ASCII glyphs into GL_RED textures
* and renders them as textured quads into whichever viewport is active.
*/
#include "02_text_description.h"
#include <ft2build.h>
#include FT_FREETYPE_H
#include <cstdio>
#include <fstream>
#include <sstream>
/* Global singleton used by all components. */
TextRenderer g_text_renderer;
/* ------------------------------------------------------------------ */
/* Internal helpers */
/* ------------------------------------------------------------------ */
static bool read_file(const std::string& path, std::string& out) {
std::ifstream f(path);
if (!f.is_open()) {
fprintf(stderr, "[02_text] cannot open shader: %s\n", path.c_str());
return false;
}
std::ostringstream ss;
ss << f.rdbuf();
out = ss.str();
return true;
}
static GLuint compile_shader_src(const char* source, GLenum type) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512];
glGetShaderInfoLog(shader, 512, nullptr, log);
fprintf(stderr, "[02_text] shader compile error:\n%s\n", log);
}
return shader;
}
/* ------------------------------------------------------------------ */
/* TextRenderer public methods */
/* ------------------------------------------------------------------ */
bool TextRenderer::initialize_shaders() {
std::string vert_src, frag_src;
if (!read_file(SHADER_DIR "02_text_description_vert.glsl", vert_src)) return false;
if (!read_file(SHADER_DIR "02_text_description_frag.glsl", frag_src)) return false;
GLuint vert = compile_shader_src(vert_src.c_str(), GL_VERTEX_SHADER);
GLuint frag = compile_shader_src(frag_src.c_str(), GL_FRAGMENT_SHADER);
shader_program_ = glCreateProgram();
glAttachShader(shader_program_, vert);
glAttachShader(shader_program_, frag);
glLinkProgram(shader_program_);
GLint ok = 0;
glGetProgramiv(shader_program_, GL_LINK_STATUS, &ok);
if (!ok) {
char log[512];
glGetProgramInfoLog(shader_program_, 512, nullptr, log);
fprintf(stderr, "[02_text] shader link error:\n%s\n", log);
glDeleteShader(vert);
glDeleteShader(frag);
return false;
}
glDeleteShader(vert);
glDeleteShader(frag);
/* VAO/VBO for one glyph quad: 6 vertices × 4 floats (xy + uv). */
glGenVertexArrays(1, &vao_);
glGenBuffers(1, &vbo_);
glBindVertexArray(vao_);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast<void*>(0));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
/* Bind text_sampler uniform to texture unit 0. */
glUseProgram(shader_program_);
glUniform1i(glGetUniformLocation(shader_program_, "text_sampler"), 0);
glUseProgram(0);
#if DEBUG_SHADER_COMPILE
fprintf(stdout, "[02_text] shaders compiled and linked OK\n");
#endif
return true;
}
bool TextRenderer::initialize_font(const std::string& font_path, unsigned int font_size) {
FT_Library ft_lib;
if (FT_Init_FreeType(&ft_lib)) {
fprintf(stderr, "[02_text] FreeType init failed\n");
return false;
}
FT_Face ft_face;
if (FT_New_Face(ft_lib, font_path.c_str(), 0, &ft_face)) {
fprintf(stderr, "[02_text] cannot load font: %s\n", font_path.c_str());
FT_Done_FreeType(ft_lib);
return false;
}
FT_Set_Pixel_Sizes(ft_face, 0, font_size);
/* Upload each printable ASCII glyph as a GL_RED texture. */
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
for (unsigned char c = 32; c < 127; c++) {
if (FT_Load_Char(ft_face, c, FT_LOAD_RENDER)) {
fprintf(stderr, "[02_text] failed to load glyph 0x%02x\n", c);
continue;
}
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(
GL_TEXTURE_2D, 0, GL_RED,
static_cast<GLsizei>(ft_face->glyph->bitmap.width),
static_cast<GLsizei>(ft_face->glyph->bitmap.rows),
0, GL_RED, GL_UNSIGNED_BYTE,
ft_face->glyph->bitmap.buffer
);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glyph_character ch;
ch.texture_id = tex;
ch.size_x = static_cast<int>(ft_face->glyph->bitmap.width);
ch.size_y = static_cast<int>(ft_face->glyph->bitmap.rows);
ch.bearing_x = ft_face->glyph->bitmap_left;
ch.bearing_y = ft_face->glyph->bitmap_top;
ch.advance = static_cast<unsigned int>(ft_face->glyph->advance.x);
characters_[static_cast<char>(c)] = ch;
}
glBindTexture(GL_TEXTURE_2D, 0);
FT_Done_Face(ft_face);
FT_Done_FreeType(ft_lib);
fprintf(stdout, "[02_text] font loaded: %s at %u px\n", font_path.c_str(), font_size);
return true;
}
void TextRenderer::render_text(const std::string& text,
float x, float y, float scale,
float color_r, float color_g, float color_b) {
glUseProgram(shader_program_);
glUniform3f(glGetUniformLocation(shader_program_, "text_color"),
color_r, color_g, color_b);
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(vao_);
for (std::string::const_iterator it = text.cbegin(); it != text.cend(); ++it) {
char c = *it;
if (characters_.count(c) == 0) {
/* Advance by a fixed amount for unknown characters. */
x += 10.0f * scale;
continue;
}
const glyph_character& ch = characters_.at(c);
float xpos = x + static_cast<float>(ch.bearing_x) * scale;
float ypos = y - static_cast<float>(ch.size_y - ch.bearing_y) * scale;
float w = static_cast<float>(ch.size_x) * scale;
float h = static_cast<float>(ch.size_y) * scale;
if (w > 0.0f && h > 0.0f) {
float vertices[6][4] = {
{ xpos, ypos + h, 0.0f, 0.0f },
{ xpos, ypos, 0.0f, 1.0f },
{ xpos + w, ypos, 1.0f, 1.0f },
{ xpos, ypos + h, 0.0f, 0.0f },
{ xpos + w, ypos, 1.0f, 1.0f },
{ xpos + w, ypos + h, 1.0f, 0.0f }
};
glBindTexture(GL_TEXTURE_2D, ch.texture_id);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
x += static_cast<float>(ch.advance >> 6) * scale;
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
}
float TextRenderer::get_text_width(const std::string& text, float scale) const {
float width = 0.0f;
for (std::string::const_iterator it = text.cbegin(); it != text.cend(); ++it) {
char c = *it;
if (characters_.count(c) == 0) {
width += 10.0f * scale;
continue;
}
width += static_cast<float>(characters_.at(c).advance >> 6) * scale;
}
return width;
}
void TextRenderer::set_projection(int w, int h) {
/* Column-major orthographic matrix mapping pixel space [0,w]x[0,h]
* to NDC. Near=-1, far=1 (depth unused for 2D text). */
float fw = static_cast<float>(w);
float fh = static_cast<float>(h);
float proj[16] = {
2.0f / fw, 0.0f, 0.0f, 0.0f,
0.0f, 2.0f / fh, 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f, 1.0f
};
glUseProgram(shader_program_);
glUniformMatrix4fv(
glGetUniformLocation(shader_program_, "projection"),
1, GL_FALSE, proj
);
glUseProgram(0);
}
void TextRenderer::cleanup() {
for (std::map<char, glyph_character>::iterator it = characters_.begin();
it != characters_.end(); ++it) {
glDeleteTextures(1, &it->second.texture_id);
}
characters_.clear();
if (vao_) { glDeleteVertexArrays(1, &vao_); vao_ = 0; }
if (vbo_) { glDeleteBuffers(1, &vbo_); vbo_ = 0; }
if (shader_program_) { glDeleteProgram(shader_program_); shader_program_ = 0; }
}

711
src/main.cpp Normal file
View File

@@ -0,0 +1,711 @@
/*
* MIT License
*
* Copyright (c) 2026 Mark Allyn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* main.cpp
* Author: Mark Allyn
* Component: 03_setup_and_radar_selection (spec: 03_setup_and_radar_selection.md)
*
* Entry point. Handles:
* - Command-line parsing to set radar availability.
* - OpenGL / GLFW / GLAD initialisation (Thread 1).
* - GL debug output callback.
* - Shader and font loading for all components.
* - Radar selection loop (intro screen).
* - Main render loop at ~30 Hz.
* - Keyboard callbacks for radar selection and scope controls.
*
* Thread: Thread 1 (all GL and keyboard work lives here).
*/
/* GLAD must be included before any other OpenGL header. */
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "data_structure_and_constants.h"
#include "settings.h"
#include "01_classes.h"
#include "02_text_description.h"
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <algorithm>
#include <chrono>
#include <thread>
/* ------------------------------------------------------------------ */
/* Application state */
/* ------------------------------------------------------------------ */
enum class AppState {
SELECTION, /* intro / radar-selection screen */
OPERATING /* a radar is active */
};
static AppState g_app_state = AppState::SELECTION;
/* Entry in the selectable-radar list (built from radar_management[]). */
struct selectable_radar {
int management_index; /* RADAR_* constant */
std::string display_name;
RadarBase* instance;
};
static std::vector<selectable_radar> g_selectable;
static int g_selection_cursor = 0;
/* Pointer to the currently operating radar object (null in SELECTION state). */
static RadarBase* g_active_radar = nullptr;
/* ------------------------------------------------------------------ */
/* Colour constants for selection / intro rendering */
/* ------------------------------------------------------------------ */
static const float COL_WHITE[3] = { 1.00f, 1.00f, 1.00f };
static const float COL_YELLOW[3] = { 1.00f, 1.00f, 0.00f };
static const float COL_GREEN_HI[3] = { 0.20f, 1.00f, 0.40f }; /* highlighted item */
static const float COL_GREEN_DIM[3] = { 0.00f, 0.65f, 0.22f }; /* non-selected item */
static const float COL_PINK[3] = { 1.00f, 0.60f, 0.80f };
static const float COL_RED[3] = { 1.00f, 0.25f, 0.25f };
static const float LH_MAIN = LINE_HEIGHT;
static const float MARGIN = static_cast<float>(TEXT_MARGIN);
/* ------------------------------------------------------------------ */
/* GL debug callback (GPU robustness protocol) */
/* ------------------------------------------------------------------ */
static void GLAPIENTRY gl_debug_callback(GLenum /*source*/,
GLenum type,
GLuint /*id*/,
GLenum severity,
GLsizei /*length*/,
const GLchar* message,
const void* /*user_param*/) {
/* Skip verbose notifications to keep output readable. */
if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;
const char* type_str = "UNKNOWN";
switch (type) {
case GL_DEBUG_TYPE_ERROR: type_str = "ERROR"; break;
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: type_str = "DEPRECATED"; break;
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: type_str = "UNDEFINED_BEHAVIOUR"; break;
case GL_DEBUG_TYPE_PERFORMANCE: type_str = "PERFORMANCE"; break;
case GL_DEBUG_TYPE_PORTABILITY: type_str = "PORTABILITY"; break;
default: break;
}
fprintf(stderr, "[GL %s] %s\n", type_str, message);
}
/* ------------------------------------------------------------------ */
/* Keyboard callback (Thread 1) */
/* ------------------------------------------------------------------ */
static void key_callback(GLFWwindow* window, int key, int /*scancode*/,
int action, int /*mods*/) {
if (action != GLFW_PRESS && action != GLFW_REPEAT) return;
if (key == GLFW_KEY_ESCAPE) {
glfwSetWindowShouldClose(window, GLFW_TRUE);
return;
}
/* ---- SELECTION state ---- */
if (g_app_state == AppState::SELECTION) {
if (g_selectable.empty()) return;
if (key == GLFW_KEY_1) {
g_selection_cursor =
(g_selection_cursor + 1) % static_cast<int>(g_selectable.size());
} else if (key == GLFW_KEY_2) {
g_selection_cursor =
(g_selection_cursor - 1 + static_cast<int>(g_selectable.size()))
% static_cast<int>(g_selectable.size());
} else if (key == GLFW_KEY_ENTER || key == GLFW_KEY_KP_ENTER) {
/* Activate selected radar. */
int mi = g_selectable[g_selection_cursor].management_index;
for (int i = 0; i < RADAR_COUNT; i++) radar_management[i].selected = 0;
radar_management[mi].selected = 1;
current_radar = mi;
g_active_radar = g_selectable[g_selection_cursor].instance;
g_app_state = AppState::OPERATING;
}
return;
}
/* ---- OPERATING state ---- */
/* '1' returns to selection from any operating radar. */
if (key == GLFW_KEY_1) {
radar_management[current_radar].selected = 0;
current_radar = RADAR_NONE;
g_active_radar = nullptr;
g_app_state = AppState::SELECTION;
return;
}
/* Scope controls — applied to whichever radar is active. */
switch (key) {
/* Intensity (all scopes) */
case GLFW_KEY_3:
uniform_intensity = std::max(INTENSITY_MIN,
uniform_intensity - INTENSITY_STEP);
break;
case GLFW_KEY_4:
uniform_intensity = std::min(INTENSITY_MAX,
uniform_intensity + INTENSITY_STEP);
break;
/* Receiver sensitivity (all scopes) */
case GLFW_KEY_5:
uniform_sensitivity = std::max(SENSITIVITY_MIN,
uniform_sensitivity - SENSITIVITY_STEP);
break;
case GLFW_KEY_6:
uniform_sensitivity = std::min(SENSITIVITY_MAX,
uniform_sensitivity + SENSITIVITY_STEP);
break;
/* STC sensitivity (all scopes) */
case GLFW_KEY_Q:
uniform_stc_sensitivity = std::max(STC_SENSITIVITY_MIN,
uniform_stc_sensitivity - STC_SENSITIVITY_STEP);
break;
case GLFW_KEY_W:
uniform_stc_sensitivity = std::min(STC_SENSITIVITY_MAX,
uniform_stc_sensitivity + STC_SENSITIVITY_STEP);
break;
/* STC range (all scopes) */
case GLFW_KEY_E:
uniform_stc_range = std::max(STC_RANGE_MIN,
uniform_stc_range - STC_RANGE_STEP);
break;
case GLFW_KEY_R:
uniform_stc_range = std::min(STC_RANGE_MAX,
uniform_stc_range + STC_RANGE_STEP);
break;
/* Rain noise filter (PPI scopes only) */
case GLFW_KEY_T:
if (current_radar == RADAR_MARINE_TRAFFIC ||
current_radar == RADAR_POLICE_BOAT) {
uniform_noise_filter = std::max(NOISE_FILTER_MIN,
uniform_noise_filter - NOISE_FILTER_STEP);
}
break;
case GLFW_KEY_Y:
if (current_radar == RADAR_MARINE_TRAFFIC ||
current_radar == RADAR_POLICE_BOAT) {
uniform_noise_filter = std::min(NOISE_FILTER_MAX,
uniform_noise_filter + NOISE_FILTER_STEP);
}
break;
/* Radiogoniometer (Chain Home only) */
case GLFW_KEY_U:
if (current_radar == RADAR_CHAIN_HOME) {
uniform_radiogoniometer -= RADIOGON_STEP;
if (uniform_radiogoniometer < 0.0f)
uniform_radiogoniometer += 360.0f;
}
break;
case GLFW_KEY_I:
if (current_radar == RADAR_CHAIN_HOME) {
uniform_radiogoniometer += RADIOGON_STEP;
if (uniform_radiogoniometer >= 360.0f)
uniform_radiogoniometer -= 360.0f;
}
break;
/* Max range (all scopes except Chain Home) */
case GLFW_KEY_O:
if (current_radar != RADAR_CHAIN_HOME) {
uniform_max_range = std::max(MAX_RANGE_MIN,
uniform_max_range - MAX_RANGE_STEP);
}
break;
case GLFW_KEY_P:
if (current_radar != RADAR_CHAIN_HOME) {
uniform_max_range = std::min(MAX_RANGE_MAX,
uniform_max_range + MAX_RANGE_STEP);
}
break;
/* Range cursor (PPI scopes only) */
case GLFW_KEY_A:
if (current_radar == RADAR_MARINE_TRAFFIC ||
current_radar == RADAR_POLICE_BOAT) {
uniform_range_cursor = std::max(RANGE_CURSOR_MIN,
uniform_range_cursor - RANGE_CURSOR_STEP);
}
break;
case GLFW_KEY_S:
if (current_radar == RADAR_MARINE_TRAFFIC ||
current_radar == RADAR_POLICE_BOAT) {
uniform_range_cursor = std::min(RANGE_CURSOR_MAX,
uniform_range_cursor + RANGE_CURSOR_STEP);
}
break;
/* Bearing cursor (PPI scopes only) */
case GLFW_KEY_D:
if (current_radar == RADAR_MARINE_TRAFFIC ||
current_radar == RADAR_POLICE_BOAT) {
uniform_bearing_cursor -= BEARING_CURSOR_STEP;
if (uniform_bearing_cursor < 0.0f)
uniform_bearing_cursor += 360.0f;
}
break;
case GLFW_KEY_F:
if (current_radar == RADAR_MARINE_TRAFFIC ||
current_radar == RADAR_POLICE_BOAT) {
uniform_bearing_cursor += BEARING_CURSOR_STEP;
if (uniform_bearing_cursor >= 360.0f)
uniform_bearing_cursor -= 360.0f;
}
break;
/* Marine A-scope antenna rotation */
case GLFW_KEY_G:
if (current_radar == RADAR_MARINE_ASCOPE) {
uniform_marine_a_bearing -= MARINE_A_BEARING_STEP;
if (uniform_marine_a_bearing < 0.0f)
uniform_marine_a_bearing += 360.0f;
}
break;
case GLFW_KEY_H:
if (current_radar == RADAR_MARINE_ASCOPE) {
uniform_marine_a_bearing += MARINE_A_BEARING_STEP;
if (uniform_marine_a_bearing >= 360.0f)
uniform_marine_a_bearing -= 360.0f;
}
break;
default:
break;
}
}
/* ------------------------------------------------------------------ */
/* parse_args: set radar availability from command line */
/* ------------------------------------------------------------------ */
static void parse_args(int argc, char* argv[]) {
/* Stub build: mark all four radars as operatable (fully built). */
for (int i = 1; i < RADAR_COUNT; i++) {
radar_management[i].operatable = 1;
}
if (argc <= 1) {
/* No arguments — all operatable radars are available. */
for (int i = 1; i < RADAR_COUNT; i++) {
if (radar_management[i].operatable)
radar_management[i].available = 1;
}
} else {
for (int a = 1; a < argc; a++) {
std::string arg = argv[a];
if (arg == "chain_home") radar_management[RADAR_CHAIN_HOME].available = 1;
else if (arg == "marine_ascope") radar_management[RADAR_MARINE_ASCOPE].available = 1;
else if (arg == "marine_traffic")radar_management[RADAR_MARINE_TRAFFIC].available = 1;
else if (arg == "police_boat") radar_management[RADAR_POLICE_BOAT].available = 1;
else fprintf(stderr, "[main] unknown radar name ignored: %s\n", arg.c_str());
}
}
/* Build the selectable list in the canonical order. */
g_selectable.clear();
struct radar_meta { int idx; const char* name; RadarBase* obj; };
radar_meta entries[4] = {
{ RADAR_CHAIN_HOME, "Chain Home Radar", &g_chain_home },
{ RADAR_MARINE_ASCOPE, "Marine A-Scope Radar", &g_marine_ascope},
{ RADAR_MARINE_TRAFFIC, "Marine Traffic Control Radar", &g_marine_traffic},
{ RADAR_POLICE_BOAT, "Police Boat Radar", &g_police_boat },
};
for (int e = 0; e < 4; e++) {
if (radar_management[entries[e].idx].available) {
selectable_radar sr;
sr.management_index = entries[e].idx;
sr.display_name = entries[e].name;
sr.instance = entries[e].obj;
g_selectable.push_back(sr);
}
}
if (g_selectable.empty()) {
fprintf(stderr, "[main] WARNING: no radars are available. "
"Run with no args to enable all.\n");
}
fprintf(stdout, "[main] %zu radar(s) available.\n", g_selectable.size());
}
/* ------------------------------------------------------------------ */
/* Intro / selection screen rendering */
/* ------------------------------------------------------------------ */
static void render_intro_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH_MAIN * 1.25f;
float x = MARGIN;
g_text_renderer.render_text("RAF / MARINE RADAR EXHIBIT", x, y, 1.25f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
y -= LH_MAIN * 1.25f;
y -= LH_MAIN * 0.5f;
const char* intro[] = {
"Welcome to the radar exhibit.",
"",
"This exhibit lets you experience",
"radar technology from the 1940s",
"and 1950s — from the Chain Home",
"system that helped defend Britain",
"in World War 2, to the marine PPI",
"radars used by the Coast Guard",
"and police today.",
"",
"Use the controls on the right to",
"select a radar and explore it.",
nullptr
};
for (int i = 0; intro[i] != nullptr; i++) {
if (intro[i][0] == '\0') {
y -= LH_MAIN * 0.5f;
} else {
g_text_renderer.render_text(intro[i], x, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
y -= LH_MAIN;
}
}
}
static void render_radar_list() {
float scope_w = static_cast<float>(SCOPE_WIDTH);
float scope_h = static_cast<float>(SCOPE_HEIGHT);
/* Title */
std::string title = "SELECT A RADAR";
float tw = g_text_renderer.get_text_width(title, 1.5f);
g_text_renderer.render_text(title,
(scope_w - tw) / 2.0f,
scope_h - 60.0f,
1.5f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
if (g_selectable.empty()) {
std::string msg = "No radars available. Restart with radar names as arguments.";
float mw = g_text_renderer.get_text_width(msg, 1.0f);
g_text_renderer.render_text(msg,
(scope_w - mw) / 2.0f,
scope_h / 2.0f,
1.0f,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
return;
}
/* List each available radar; highlight the current selection. */
int count = static_cast<int>(g_selectable.size());
float list_y_start = scope_h / 2.0f + (count * LH_MAIN * 1.4f) / 2.0f;
for (int i = 0; i < count; i++) {
std::string entry = g_selectable[i].display_name;
bool highlighted = (i == g_selection_cursor);
float scale = highlighted ? 1.3f : 1.0f;
const float* col = highlighted ? COL_GREEN_HI : COL_GREEN_DIM;
std::string prefix = highlighted ? "> " : " ";
std::string line = prefix + entry;
float lw = g_text_renderer.get_text_width(line, scale);
float lx = (scope_w - lw) / 2.0f;
float ly = list_y_start - i * LH_MAIN * 1.4f;
g_text_renderer.render_text(line, lx, ly, scale,
col[0], col[1], col[2]);
}
}
static void render_selection_status_bar() {
float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH_MAIN;
g_text_renderer.render_text("Use ", x, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
float xp = x + g_text_renderer.get_text_width("Use ", 1.0f);
g_text_renderer.render_text("1", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("1", 1.0f);
g_text_renderer.render_text(" / ", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
xp += g_text_renderer.get_text_width(" / ", 1.0f);
g_text_renderer.render_text("2", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("2", 1.0f);
g_text_renderer.render_text(" to move the selection. Press ", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
xp += g_text_renderer.get_text_width(" to move the selection. Press ", 1.0f);
g_text_renderer.render_text("ENTER", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("ENTER", 1.0f);
g_text_renderer.render_text(" to activate.", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
y -= LH_MAIN;
g_text_renderer.render_text("Press ", x, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
xp = x + g_text_renderer.get_text_width("Press ", 1.0f);
g_text_renderer.render_text("ESC", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("ESC", 1.0f);
g_text_renderer.render_text(" to exit the exhibit.", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
}
/* ------------------------------------------------------------------ */
/* Viewport rendering orchestrators */
/* ------------------------------------------------------------------ */
static void render_selection_screen() {
int scope_x = LEFT_PANEL_WIDTH;
int scope_y = STATUS_BAR_HEIGHT;
int scope_w = SCOPE_WIDTH;
int scope_h = SCOPE_HEIGHT;
/* Left panel */
glViewport(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glScissor(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glEnable(GL_SCISSOR_TEST);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
render_intro_left_panel();
/* Scope area — radar list */
glViewport(scope_x, scope_y, scope_w, scope_h);
glScissor(scope_x, scope_y, scope_w, scope_h);
glClearColor(0.0f, 0.05f, 0.01f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, scope_h);
render_radar_list();
/* Status bar */
glViewport(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glScissor(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, STATUS_BAR_HEIGHT);
render_selection_status_bar();
glDisable(GL_SCISSOR_TEST);
}
static void render_operating_screen() {
int scope_x = LEFT_PANEL_WIDTH;
int scope_y = STATUS_BAR_HEIGHT;
int scope_w = SCOPE_WIDTH;
int scope_h = SCOPE_HEIGHT;
/* Left panel — radar description and controls */
glViewport(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glScissor(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glEnable(GL_SCISSOR_TEST);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
g_active_radar->render_left_panel();
/* Scope — radar name (stub) */
glViewport(scope_x, scope_y, scope_w, scope_h);
glScissor(scope_x, scope_y, scope_w, scope_h);
glClearColor(0.0f, 0.05f, 0.01f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, scope_h);
g_active_radar->render_scope();
/* Status bar — current control values */
glViewport(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glScissor(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, STATUS_BAR_HEIGHT);
g_active_radar->render_status_bar();
glDisable(GL_SCISSOR_TEST);
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
int main(int argc, char* argv[]) {
/* 1. Parse command-line arguments to set radar availability. */
parse_args(argc, argv);
/* 2. Initialise GLFW. */
if (!glfwInit()) {
fprintf(stderr, "[main] GLFW init failed\n");
return 1;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#if ENABLE_GL_DEBUG_OUTPUT
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
#endif
GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT,
WINDOW_TITLE, nullptr, nullptr);
if (!window) {
fprintf(stderr, "[main] GLFW window creation failed\n");
glfwTerminate();
return 1;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(1); /* vsync */
/* 3. Initialise GLAD. */
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(glfwGetProcAddress))) {
fprintf(stderr, "[main] GLAD init failed\n");
glfwTerminate();
return 1;
}
fprintf(stdout, "[main] OpenGL %s — %s\n",
glGetString(GL_VERSION), glGetString(GL_RENDERER));
/* 4. Enable GL debug output. */
#if ENABLE_GL_DEBUG_OUTPUT
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(gl_debug_callback, nullptr);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE,
GL_DEBUG_SEVERITY_NOTIFICATION,
0, nullptr, GL_FALSE);
fprintf(stdout, "[main] GL debug output enabled\n");
#endif
/* 5. Set global GL state. */
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
/* 6. Load text renderer shaders and font. */
if (!g_text_renderer.initialize_shaders()) {
fprintf(stderr, "[main] text renderer shader init failed\n");
glfwTerminate();
return 1;
}
if (!g_text_renderer.initialize_font(FONT_PATH, FONT_SIZE_NORMAL)) {
fprintf(stderr, "[main] font load failed: " FONT_PATH "\n");
glfwTerminate();
return 1;
}
/* 7. Register keyboard callback. */
glfwSetKeyCallback(window, key_callback);
/* 8. If exactly one radar is available, auto-activate after 5 seconds. */
if (g_selectable.size() == 1) {
fprintf(stdout, "[main] single radar available; activating in 5 s...\n");
using clock = std::chrono::steady_clock;
clock::time_point deadline = clock::now() + std::chrono::seconds(5);
while (!glfwWindowShouldClose(window) && clock::now() < deadline) {
glfwPollEvents();
glDisable(GL_SCISSOR_TEST);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
render_selection_screen();
glfwSwapBuffers(window);
std::this_thread::sleep_for(std::chrono::milliseconds(33));
/* Break early if the user pressed Enter. */
if (g_app_state == AppState::OPERATING) break;
}
/* Auto-activate if the user has not already pressed Enter. */
if (g_app_state == AppState::SELECTION && !glfwWindowShouldClose(window)) {
radar_management[g_selectable[0].management_index].selected = 1;
current_radar = g_selectable[0].management_index;
g_active_radar = g_selectable[0].instance;
g_app_state = AppState::OPERATING;
}
}
/* 9. Main render loop at ~30 Hz. */
using clock = std::chrono::steady_clock;
const std::chrono::duration<double> frame_time(1.0 / 30.0);
while (!glfwWindowShouldClose(window)) {
clock::time_point frame_start = clock::now();
glfwPollEvents();
/* Clear full window to a near-black background. */
glDisable(GL_SCISSOR_TEST);
glClearColor(0.02f, 0.02f, 0.02f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (g_app_state == AppState::SELECTION) {
render_selection_screen();
} else {
render_operating_screen();
}
glfwSwapBuffers(window);
/* Sleep for the remainder of the frame period. */
std::chrono::duration<double> elapsed = clock::now() - frame_start;
if (elapsed < frame_time) {
std::this_thread::sleep_for(frame_time - elapsed);
}
}
/* 10. Cleanup. */
g_text_renderer.cleanup();
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}