From 0dc329c9932a63ac431c0199c7e44bb36bb3bd89 Mon Sep 17 00:00:00 2001 From: mcfnord Date: Sat, 8 Aug 2026 19:39:28 +0000 Subject: [PATCH 1/6] src/README.md: map the source folder, its threads and its locks --- src/README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/README.md diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000000..a7af8595ab --- /dev/null +++ b/src/README.md @@ -0,0 +1,121 @@ +### Copyright (c) 2026 + +Author(s): +* mcfnord +* The Jamulus Development Team + +As of Jamulus 3.12.1dev (commit eb172d47): All new source code contributions must be licensed +under AGPL 3.0 or any later version. + +--- + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see [](https://www.gnu.org/licenses/). + +# The src folder + +This file is a map of `src/` for a reader new to the code: which class lives where, which +threads exist at runtime, and which lock protects what. Like [sound/README.md](sound/README.md), +it describes how the code behaves today, not why it was designed that way. It is not complete; +the last section lists what is still missing. + +## Where things live + +Code used by both client and server: + +- [main.cpp](main.cpp) parses the command line and constructs a `CClient` or a `CServer`. +- [protocol.cpp](protocol.cpp) — `CProtocol`: protocol message framing, acknowledgement, and + retransmission of unacknowledged messages from `SendMessQueue`. The wire format itself is + described in [../docs/JAMULUS_PROTOCOL.md](../docs/JAMULUS_PROTOCOL.md). +- [channel.cpp](channel.cpp) — `CChannel`: one connection, holding the receive jitter buffer + (`SockBuf`) and a `CProtocol` instance. The client has one; the server has a fixed array of + `MAX_NUM_CHANNELS` of them (`vecChannels`). +- [socket.cpp](socket.cpp) — `CSocket`, wrapped in `CHighPrioSocket` together with its receive + thread: the UDP socket shared by all sending and receiving. +- [buffer.h](buffer.h) — `CNetBuf` and `CNetBufWithStats`: the jitter buffer itself, including + the automatic size decision. +- [util.h](util.h) / [util.cpp](util.cpp) — `CHighPrecisionTimer` (the server's frame clock) + and assorted helpers. + +Client only: [client.cpp](client.cpp) (`CClient`), the sound layer in [sound/](sound/), the GUI +([clientdlg.cpp](clientdlg.cpp), [clientsettingsdlg.cpp](clientsettingsdlg.cpp), +[audiomixerboard.cpp](audiomixerboard.cpp), [connectdlg.cpp](connectdlg.cpp), +[chatdlg.cpp](chatdlg.cpp)), and [clientrpc.cpp](clientrpc.cpp). + +Server only: [server.cpp](server.cpp) (`CServer`: the channels and the mix), +[serverlist.cpp](serverlist.cpp) (directory registration and the server list), +[recorder/](recorder/), [serverlogging.cpp](serverlogging.cpp), +[serverrpc.cpp](serverrpc.cpp) and [serverdlg.cpp](serverdlg.cpp). + +The JSON-RPC API ([rpcserver.cpp](rpcserver.cpp), [clientrpc.cpp](clientrpc.cpp), +[serverrpc.cpp](serverrpc.cpp)) is documented in [../docs/JSON-RPC.md](../docs/JSON-RPC.md). + +## Threads + +| thread | exists | started from | what runs on it | +|---|---|---|---| +| Qt main thread | always | — | the GUI; every protocol message, parsed and created, on client and server; directory registration; JSON-RPC; and the server's complete frame cycle (see below) | +| `CSocketThread` | always | `CHighPrioSocket::Start()`, at `QThread::TimeCriticalPriority` | a blocking UDP receive loop. Audio packets are decoded into the jitter buffer synchronously, in `CChannel::PutAudioData` (client) or `CServer::PutAudioData` (server). Protocol frames are not parsed here: they are re-emitted as queued signals and handled on the main thread. | +| audio driver threads | client | the sound driver | the backend callback, which runs `CClient::AudioCallback`: Opus decode of the received stream, Opus encode of the sound card input, and the UDP send of the encoded packet | +| `CHighPrecisionT…` | server, except on Windows | `CHighPrecisionTimer::Start()`, at `QThread::TimeCriticalPriority` | only `emit timeout()` once per frame, plus the absolute-time sleep that paces it | +| `CThreadPool` workers | server with `--multithreading` | `CServer`'s constructor | Opus decode and mix/encode/send work, in per-block chunks handed out by `CServer::OnTimer` | +| recorder thread | server with recording | `CJamController` | `CJamRecorder`, fed by queued `AudioFrame` signals from the frame cycle | +| `QThreadPool` global pool | client GUI | the connect dialog | one task per listed server for the ping/info fan-out (`QtConcurrent::run`) | + +Three consequences that are easy to miss: + +- **The server's frame cycle runs on the main thread.** `CHighPrecisionTimer`'s thread runs at + `QThread::TimeCriticalPriority`, but the only work it does is `emit timeout()`. The slot on the + other side of that signal, `CServer::OnTimer` — jitter buffer drain, Opus decode, mix, encode, + transmit — executes on the main thread, because the connection between the two is queued + (verified with a debugger on Linux; a TODO in [util.cpp](util.cpp) notes the same). On Windows, + `CHighPrecisionTimer` is built on a plain `QTimer`, which fires on the main thread as well. + With `--multithreading`, the heavy blocks run on the pool, but `OnTimer` still runs every + frame and waits for all futures before it returns. +- **`CSoundBase` inherits `QThread`, but no such thread ever runs.** Nothing in + [sound/](sound/) overrides `run()` or calls `start()` on it. Audio callbacks always arrive on + threads owned by the driver. +- **The client's audio send is clocked by the sound hardware; its receive is clocked by the + network.** `CClient::ProcessAudioDataIntern` sends from inside the driver callback; arriving + packets are buffered by `CSocketThread`. The two paths meet only at the jitter buffer and at + the socket send lock. + +## Locks + +The locks taken from more than one thread: + +| lock | protects | taken from | +|---|---|---| +| `CChannel::MutexSocketBuf` | the jitter buffer | put on `CSocketThread`; get from the client's driver callback or the server's frame cycle; re-init from the main thread | +| `CSocket::Mutex` | the send path of the shared UDP socket | every `SendPacket()` call: the driver callback (client), the frame cycle and pool workers (server), and protocol code on the main thread | +| `CServer::Mutex` | connect and disconnect of channels against the frame cycle | `CServer::OnTimer` holds it while it collects the connected channels and drains and decodes their jitter buffers, and releases it before mix and send; `CServer::PutAudioData` (`CSocketThread`) and the protocol slots (main thread) take it too | +| `CChannel::Mutex` | per-channel state: the enable flag, gain and pan tables, name | setters in protocol slots on the main thread; getters in the server's frame cycle | +| `CChannel::MutexConvBuf` | the send-side conversion buffer | `PrepAndSendPacket()` on the sending thread; re-init from the main thread | + +Smaller ones: `CProtocol::Mutex` (the queue of sent but not yet acknowledged messages), +`CServer::MutexChanOrder` (channel allocation in `FindChannel` and `FreeChannel`), +`CServer::MutexWelcomeMessage`, `CClient::MutexChannels` (the client-side channel number map), +`CClient::MutexGainOrPan` (the gain/pan message rate limiter), and +`CClient::MutexDriverReinit` (serializes sound device re-initialization). The sound layer's own +locks — `MutexAudioProcessCallback`, `MutexDevProperties`, and the per-backend ones — are +covered in [sound/README.md](sound/README.md). + +## Not yet documented + +- the jitter buffer's automatic size algorithm (`CNetBufWithStats`) +- the connection lifecycle: how a channel goes from first packet to connected to timed out +- the directory: registration, the server list, and the split of + [serverlist.cpp](serverlist.cpp) between the directory role and the registered-server role +- the recorder +- [serverlogging.cpp](serverlogging.cpp), [signalhandler.cpp](signalhandler.cpp), the GUI + classes, and translation loading From 61468d2ea512446fe08f42e9def2382d5ffce675 Mon Sep 17 00:00:00 2001 From: jrd Date: Sat, 8 Aug 2026 22:51:02 +0000 Subject: [PATCH 2/6] src/README.md: shorten per review Applies @ann0see's review on #3875: - Intro cut to two sentences; the paragraph about what the file does and does not assert is gone. - File list back to one line each: the SendMessQueue detail, the SockBuf and CProtocol members and the vecChannels name are all readable in the file itself. Kept "the client has one; the server an array of MAX_NUM_CHANNELS", which is in server.h, not channel.cpp. - The three-bullet block after the thread table is one paragraph. The CSoundBase QThread note moves to src/sound/README.md (#3873), where a reader meets the class; the send/receive clocking bullet is dropped, as the table above already carries it. The parenthetical about how the thread identities were checked is dropped too: it describes the method, not the code, and the util.cpp TODO makes the point on its own. 122 lines to 105. No claim changed. --- src/README.md | 51 ++++++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/src/README.md b/src/README.md index a7af8595ab..3ebe1cee64 100644 --- a/src/README.md +++ b/src/README.md @@ -24,28 +24,24 @@ along with this program. If not, see [](https:// # The src folder -This file is a map of `src/` for a reader new to the code: which class lives where, which -threads exist at runtime, and which lock protects what. Like [sound/README.md](sound/README.md), -it describes how the code behaves today, not why it was designed that way. It is not complete; -the last section lists what is still missing. +A map of `src/`: which class lives where, which threads exist at runtime, and which lock +protects what. The last section lists what is not covered yet. ## Where things live Code used by both client and server: - [main.cpp](main.cpp) parses the command line and constructs a `CClient` or a `CServer`. -- [protocol.cpp](protocol.cpp) — `CProtocol`: protocol message framing, acknowledgement, and - retransmission of unacknowledged messages from `SendMessQueue`. The wire format itself is - described in [../docs/JAMULUS_PROTOCOL.md](../docs/JAMULUS_PROTOCOL.md). -- [channel.cpp](channel.cpp) — `CChannel`: one connection, holding the receive jitter buffer - (`SockBuf`) and a `CProtocol` instance. The client has one; the server has a fixed array of - `MAX_NUM_CHANNELS` of them (`vecChannels`). -- [socket.cpp](socket.cpp) — `CSocket`, wrapped in `CHighPrioSocket` together with its receive - thread: the UDP socket shared by all sending and receiving. -- [buffer.h](buffer.h) — `CNetBuf` and `CNetBufWithStats`: the jitter buffer itself, including - the automatic size decision. -- [util.h](util.h) / [util.cpp](util.cpp) — `CHighPrecisionTimer` (the server's frame clock) - and assorted helpers. +- [protocol.cpp](protocol.cpp) — `CProtocol`: message framing, acknowledgement and + retransmission. Wire format: [../docs/JAMULUS_PROTOCOL.md](../docs/JAMULUS_PROTOCOL.md). +- [channel.cpp](channel.cpp) — `CChannel`: the connection and its receive jitter buffer, used by + both client and server. The client has one; the server an array of `MAX_NUM_CHANNELS`. +- [socket.cpp](socket.cpp) — `CSocket` and `CHighPrioSocket`: the UDP socket shared by all + sending and receiving, with its receive thread. +- [buffer.h](buffer.h) — `CNetBuf` and `CNetBufWithStats`: the jitter buffer, including the + automatic size decision. +- [util.h](util.h) / [util.cpp](util.cpp) — `CHighPrecisionTimer`, the server's frame clock, and + assorted helpers. Client only: [client.cpp](client.cpp) (`CClient`), the sound layer in [sound/](sound/), the GUI ([clientdlg.cpp](clientdlg.cpp), [clientsettingsdlg.cpp](clientsettingsdlg.cpp), @@ -72,23 +68,12 @@ The JSON-RPC API ([rpcserver.cpp](rpcserver.cpp), [clientrpc.cpp](clientrpc.cpp) | recorder thread | server with recording | `CJamController` | `CJamRecorder`, fed by queued `AudioFrame` signals from the frame cycle | | `QThreadPool` global pool | client GUI | the connect dialog | one task per listed server for the ping/info fan-out (`QtConcurrent::run`) | -Three consequences that are easy to miss: - -- **The server's frame cycle runs on the main thread.** `CHighPrecisionTimer`'s thread runs at - `QThread::TimeCriticalPriority`, but the only work it does is `emit timeout()`. The slot on the - other side of that signal, `CServer::OnTimer` — jitter buffer drain, Opus decode, mix, encode, - transmit — executes on the main thread, because the connection between the two is queued - (verified with a debugger on Linux; a TODO in [util.cpp](util.cpp) notes the same). On Windows, - `CHighPrecisionTimer` is built on a plain `QTimer`, which fires on the main thread as well. - With `--multithreading`, the heavy blocks run on the pool, but `OnTimer` still runs every - frame and waits for all futures before it returns. -- **`CSoundBase` inherits `QThread`, but no such thread ever runs.** Nothing in - [sound/](sound/) overrides `run()` or calls `start()` on it. Audio callbacks always arrive on - threads owned by the driver. -- **The client's audio send is clocked by the sound hardware; its receive is clocked by the - network.** `CClient::ProcessAudioDataIntern` sends from inside the driver callback; arriving - packets are buffered by `CSocketThread`. The two paths meet only at the jitter buffer and at - the socket send lock. +**The server's frame cycle runs on the main thread.** The `CHighPrecisionTimer` thread only +emits `timeout()`; the slot behind that queued connection, `CServer::OnTimer`, does the jitter +buffer drain, decode, mix, encode and transmit — on the main thread. The TODO in +[util.cpp](util.cpp) notes the same escape from the timer thread. On Windows the pacer is a +plain `QTimer`, also main thread. With `--multithreading` the heavy blocks go to the pool, but +`OnTimer` waits for them. ## Locks From 677a0d97cee175ce5bf9c7e6c6cd32310b6237bf Mon Sep 17 00:00:00 2001 From: John Dempsey <1750243+mcfnord@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:26:03 -0700 Subject: [PATCH 3/6] Apply suggestion from @mcfnord --- src/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/README.md b/src/README.md index 3ebe1cee64..65b7447995 100644 --- a/src/README.md +++ b/src/README.md @@ -27,7 +27,7 @@ along with this program. If not, see [](https:// A map of `src/`: which class lives where, which threads exist at runtime, and which lock protects what. The last section lists what is not covered yet. -## Where things live +# Where things live Code used by both client and server: From 9b59d4fe8a1d3f30bfdd22296579de57d2d88397 Mon Sep 17 00:00:00 2001 From: John Dempsey <1750243+mcfnord@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:26:10 -0700 Subject: [PATCH 4/6] Apply suggestion from @mcfnord --- src/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/README.md b/src/README.md index 65b7447995..f8da958968 100644 --- a/src/README.md +++ b/src/README.md @@ -22,7 +22,6 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see [](https://www.gnu.org/licenses/). -# The src folder A map of `src/`: which class lives where, which threads exist at runtime, and which lock protects what. The last section lists what is not covered yet. From 691c1c5856e1f18ca739ec2610782bda4279877d Mon Sep 17 00:00:00 2001 From: John Dempsey <1750243+mcfnord@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:26:18 -0700 Subject: [PATCH 5/6] Apply suggestion from @mcfnord --- src/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/README.md b/src/README.md index f8da958968..e758cecc36 100644 --- a/src/README.md +++ b/src/README.md @@ -23,7 +23,6 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see [](https://www.gnu.org/licenses/). -A map of `src/`: which class lives where, which threads exist at runtime, and which lock protects what. The last section lists what is not covered yet. # Where things live From 24b85662e71f8b24f1665111c7b754cf798a3535 Mon Sep 17 00:00:00 2001 From: John Dempsey <1750243+mcfnord@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:26:25 -0700 Subject: [PATCH 6/6] Apply suggestion from @mcfnord --- src/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/README.md b/src/README.md index e758cecc36..6c9cd75a55 100644 --- a/src/README.md +++ b/src/README.md @@ -23,7 +23,6 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see [](https://www.gnu.org/licenses/). -protects what. The last section lists what is not covered yet. # Where things live