Inside the SWMM 6 GUI

SWMMBobSWMM6/openswmm.gui @ swmm6_gui · read at — · —

A ground-up replacement for epaswmm5.exe

SWMMVis is a Qt6/C++20 GIS-based interface for the OpenSWMM 6.0 engine. It is not a port of the Delphi GUI and it is not a wrapper around it — it is a second GUI living in the same git repository as the first, which is still sitting on master exactly as EPA left it.

The fork chain

Two GUIs, one repository

Commit 2 of this repo imported the EPA Delphi source. Everything after it is the replacement. Both are still here — you can check out either one.

What it does

Where the code is

30 buckets — 28 module directories under src/ with matching headers, one header-only module, and the handful of files at the root of src/. Bar length is lines of code, source plus headers.

Architecture

Three ideas carry most of the design: the engine holds the model, layers stack in a canvas that renders through two interchangeable paths, and every edit is an undo command.

1. The engine is the model

There is no parallel network object graph in the GUI. When the Simulation Options dialog opens, it reads every option back out of the engine through swmm_options_get rather than trusting a cached copy. When a property panel shows a node, it asks the engine what kind of node it is. The GUI keeps render-side caches — geometry structs, k-d trees, LOD pyramids — but those are derived, and they carry explicit rebuild flags.

The clearest evidence is a file that exists only to police this: include/ui/swmmvis_hydration_audit.h. It is a table of status-bar widgets paired with the engine option key each one reflects, and a regression test walks it asserting agreement at three trigger points — project open, tab activation, external mutation. Add a status-bar widget that shows an [OPTIONS] value, add a row, and the test picks it up automatically.


  

2. The layer stack

Everything on the canvas is a layer with a common base (OpenSWMMVisLayer). Bottom to top: basemaps and GIS data, then the 2D mesh, then 2D results, then the 1D network, then annotations.

3. Two render paths that must never both paint

Each heavy layer has a Qt Scene Graph renderer (GPU) and a QPainter painter (CPU). qsgOwnsRendering() gates the painter so the two pipelines cannot double-paint. The fallback is not dead code — a masked mesh, or a 2D results layer forced onto the CPU, deliberately drags the mesh back to QPainter, because if one layer went to the GPU and the layer above it did not, the stacking order would invert.

4. Undo is the edit API

29 command classes on a MapUndoStack with a configurable depth. Note the two at the end — inserting and fusing a virtual junction are undoable map operations, not a text edit in the .inp.

Module map

The engine binding

The GUI talks to openswmm.engine through a plain C API. This is the widest part of the surface and the part most likely to break when the engine moves.

Headers consumed

Bar length is include-site count across src/ and include/.

The run lifecycle

SimulationRunner uses the full nine-call lifecycle rather than the one-shot swmm_run, specifically so progress and warning callbacks can be registered. The step loop runs on a QtConcurrent worker; the static C callbacks post back to the GUI thread through QMetaObject::invokeMethod with a queued connection.

Cancel is a flush, not a kill. Cancelling exits the step loop at the next iteration but still runs end, report and close, so the partial .out and .rpt reach disk. You keep whatever the engine produced up to that moment.

It can still drive a 5.x engine

SimulationRunner branches on an engineVersion string that travels per-session in the .oswp: the legacy worker for 5.x, the refactored engine for 6.0.0+. One project can pin one engine while another pins a different one.

// src/simulation/simulationrunner.cpp:267
// Use legacy worker for 5.x versions, refactored engine for 6.0.0+

FV solver options — a live drift check

Both trees were cloned on the same day. The dialog names 20 FV_ keys; the engine's OptionsHandler.cpp parses 22. Every key the GUI names is real — the drift is one-directional.

Component catalogue

361 components across 14 groups, with the doxygen brief pulled straight from the header and the line count summing header plus implementation. Search matches the name and the description.

2D meshing and the million-cell problem

The largest single engineering effort in the branch. Generating a mesh is one problem; drawing nine million triangles without freezing the window is a different one.

The generation pipeline

Click a stage to read what it does and what went wrong in it.

Triangle is extended, never forked

vendor/triangle/ holds exactly two files — triangle.c and triangle.h — and they are untouched. Cancellation, progress reporting and a graded size function all ride on Triangle's documented triunsuitable() / EXTERNAL_TEST extension point, left undefined in the library target and resolved at the final link.

Cancellation is the interesting one. Refinement was previously uninterruptible, so the Stop button was dead for its whole duration. The fix is not a longjmp — that would leak the mesh pools, the dominant allocation in the pipeline at roughly 200 bytes per output vertex. Instead, once cancelled, the hook reports every triangle as suitable, the bad-triangle queue drains naturally, and triangulate() unwinds through its own triangledeinit().

The hook is stored thread-locally behind an RAII guard, because triunsuitable() takes no user-data pointer and concurrent triangulation still has to be correct.

Measured load performance

From the alpha.3 release notes. These are the numbers that justify the scene-graph work.

The 65,532 chunk

Qt's batch renderer addresses batched vertices with 16-bit indices, so a single geometry node above 65,535 vertices wraps and draws garbage. Uploads now split at 65,532 — the largest value under 216−1 divisible by both 3 (bare triangles) and 6 (thick-segment quads), so a chunk boundary can never fall inside a primitive.

It only ever showed on macOS, where OpenGL is the forced RHI. Metal tolerates giant nodes — which is exactly why the offscreen test harness never reproduced it. The symptom read as “the mesh shows only a sliver”: a row-ordered mesh drew its first ~21k triangles and stopped.

2D sections in the .inp

What makes it a SWMM 6 GUI

Most of the interface would be recognisable to anyone who has used the Delphi GUI. These are the parts that would not be.

Virtual junctions are a first-class map object

A virtual junction is SWMM6's way of letting a chain of conduits be modelled as one hydraulic reach: the engine concatenates them and the junction becomes an ordinary interior face. In this GUI that is not a hand edit to a [VIRTUAL_JUNCTIONS] section — it is a map tool, a property adapter, its own symbol, two undo commands, and a profile view that draws the node as pipe rather than as a manhole with an editable ground depth.

The engine error family, translated

The engine rejects an ineligible virtual junction with a numeric code. The GUI carries plain-English text for all seven — three more than the four usually quoted in conversion notes.

Moving target, verified the same day. Engine HEAD on the read date is —, whose subject is “retire VIRTUAL_JUNCTION_MOMENTUM FULL, which was not mass conserving”. FULL now warns and maps to BASIC, and InpWriter stopped emitting the key altogether. The GUI never referenced VIRTUAL_JUNCTION_MOMENTUM — so it needs no change — but any hand-written .inp carrying FULL now silently gets BASIC.

The finite-volume solver, exposed as a dialog

FLOW_ROUTING FV appears in the routing combo beside Steady, Kinematic and Dynamic Wave, and the dialog grows two contextual groups when it is chosen. The dialog detects FV support by asking the engine for an FV key and seeing whether anything comes back — a build that predates the solver answers with an empty string and the groups stay disabled.

// src/ui/dialogs/simulationoptionsdialog.cpp:175
const bool fvSupported = !legacy && !getOption("FV_CFL").isEmpty();

Full key-by-key coverage is on the Engine binding tab.

Other SWMM 6 surface

Building it

Three sibling checkouts, vcpkg for the rest, and a CMake preset per platform. The awkward part is that two dependencies are sibling directories rather than package-manager entries.

Dependencies

The sibling layout


  

The engine is expected at ../openswmm.engine. Clone it as a sibling, not a submodule — the only submodule in the repo is the doxygen theme.

Configure and build

CI

Tests

Three tiers. tests/unit is pure logic with no Qt widgets. tests/gui is the bulk — widget and renderer behaviour, including an accessibility sweep and a theme-contrast test. tests/verification is different in kind: six checks that write a .out.txt transcript beside themselves, so the numbers are reviewable rather than merely green.

Findings

Everything below was checked against the cloned trees on the read date, not recalled. Each card names how it was verified. Where a number came from the project's own release notes rather than from a measurement I ran, the card says so.

Development history

278 commits. The 2023 cluster is the Delphi import; the real work is 2024 and then a sustained run from April 2026.

Who wrote it

Built in named slices

1,473 references to a “Slice” identifier across 203 distinct slices, citing ten planning documents by section number — GUI_IMPLEMENTATION_PLAN.md (27 references), RENDERING_RULE_MODEL_PLAN.md (25), RENDERING_OUTPUT_SUBLAYERS_PLAN.md (16) and seven more.

None of those plan documents are in the repository. The code cites its own specification by section number and the specification is not published with it. For anyone reading this tree cold, that is the single biggest gap: the comments assume a document you cannot open.

Recent commits

Built from a fresh clone of SWMMBobSWMM6/openswmm.gui@swmm6_gui and HydroCouple/openswmm.engine@swmm6_rel. Counts are from the working tree; performance figures marked as such come from the project's own release notes. Upstream is GPL-3.0-or-later.