···11+# Rockbox Threading Model & Rust Integration
22+33+## Overview
44+55+Rockboxd has two distinct classes of threads that must coexist:
66+77+1. **Rockbox kernel threads** — created via `create_thread()` in C, managed by the Rockbox cooperative scheduler, **must yield explicitly**.
88+2. **Rust OS threads** — created via `std::thread::spawn`, fully preemptive OS threads, completely independent of the Rockbox scheduler.
99+1010+Understanding the boundary between these two classes is critical. Crossing it incorrectly (e.g. doing a plain OS-level block inside a Rockbox kernel thread) will silently starve every other Rockbox kernel thread.
1111+1212+---
1313+1414+## The SDL Cooperative Scheduler
1515+1616+On the `sdlapp` hosted target (macOS / Linux desktop), each Rockbox kernel thread is backed by a real SDL OS thread (`SDL_CreateThread`). However, Rockbox layers a **cooperative scheduler** on top of those OS threads using a single global SDL mutex:
1717+1818+```c
1919+// firmware/target/hosted/sdl/thread-sdl.c
2020+static SDL_mutex *m; // THE global Rockbox scheduler mutex
2121+```
2222+2323+**Only the thread that holds `m` is considered "running" by the Rockbox kernel.** All other Rockbox kernel threads are blocked in `SDL_LockMutex(m)`.
2424+2525+`switch_thread()` is the scheduler's yield primitive:
2626+```c
2727+void switch_thread(void) {
2828+ SDL_UnlockMutex(m); // release the token — let another thread run
2929+ SDL_LockMutex(m); // re-acquire when it's our turn again
3030+}
3131+```
3232+3333+Every Rockbox sleep / block call eventually calls `switch_thread()`, which is the only way to hand the scheduler token to another thread.
3434+3535+### What happens without yielding
3636+3737+If a Rockbox kernel thread calls any long-running operation — including a plain Rust `thread::sleep`, `join()`, or a `tokio::block_on` — **without first releasing the mutex via `rb::system::sleep`**, the global mutex is never released. Every other Rockbox kernel thread is stuck forever in `SDL_LockMutex(m)`.
3838+3939+### The yield contract
4040+4141+Any code running in a Rockbox kernel thread that may block for more than a few milliseconds must include a yield loop:
4242+4343+```rust
4444+loop {
4545+ thread::sleep(std::time::Duration::from_millis(100)); // OS-level rest
4646+ rb::system::sleep(rb::HZ); // release Rockbox mutex
4747+}
4848+```
4949+5050+`rb::HZ` is 100 (ticks per second on the hosted target), so `sleep(rb::HZ)` sleeps for ~1 second and releases the mutex for that duration.
5151+5252+### Stack size note
5353+5454+The `stack` and `stack_size` arguments to `create_thread()` are **ignored** on the SDL hosted target (see `thread-sdl.c`: `(void)stack; (void)stack_size;`). SDL threads receive the OS default stack size (typically 8 MB on macOS). Stack overflow is not a concern on hosted.
5555+5656+---
5757+5858+## Thread Map
5959+6060+### Rockbox kernel threads (must yield)
6161+6262+| Thread | C entry point | What it does |
6363+|--------|---------------|--------------|
6464+| `server_thread` | `server_thread.c` → `start_server()` | Spawns the actix HTTP server in a Rust OS thread, then loops yielding to the Rockbox scheduler |
6565+| `broker_thread` | `broker_thread.c` → `start_broker()` | Event loop: publishes playback state to GraphQL subscriptions, scrobbles tracks, restores playlist |
6666+6767+Both threads must call `rb::system::sleep(rb::HZ)` on every loop iteration.
6868+6969+**`server_thread` pattern** (`crates/server/src/lib.rs`):
7070+```rust
7171+pub extern "C" fn start_server() {
7272+ rockbox_settings::load_settings(None).ok();
7373+ rockbox_upnp::init();
7474+7575+ // Spawn HTTP server in its own Rust OS thread — does NOT hold the
7676+ // Rockbox mutex, so it can block freely in actix/tokio.
7777+ thread::spawn(|| {
7878+ match actix_rt::System::new().block_on(run_http_server()) { ... }
7979+ });
8080+8181+ // Keep the Rockbox kernel thread alive and cooperative.
8282+ loop {
8383+ thread::sleep(std::time::Duration::from_millis(100));
8484+ rb::system::sleep(rb::HZ);
8585+ }
8686+}
8787+```
8888+8989+**`broker_thread` pattern** (`crates/server/src/lib.rs`):
9090+```rust
9191+pub extern "C" fn start_broker() {
9292+ // ... setup ...
9393+ loop {
9494+ // ... do work (check playback, publish events) ...
9595+ thread::sleep(std::time::Duration::from_millis(100));
9696+ rb::system::sleep(rb::HZ); // yield the Rockbox mutex
9797+ }
9898+}
9999+```
100100+101101+### Rust OS threads (no Rockbox scheduler involvement)
102102+103103+These are spawned with `std::thread::spawn` — they are pure OS threads and never interact with the Rockbox mutex. They are free to block indefinitely.
104104+105105+| Thread / component | Spawned from | Runtime |
106106+|--------------------|--------------|---------|
107107+| **HTTP server** (actix-web, port 6063) | `start_server()` via `thread::spawn` | `actix_rt::System` (single-thread + LocalSet per arbiter) |
108108+| **gRPC server** (tonic, port 6061) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
109109+| **GraphQL server** (async-graphql, port 6062) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
110110+| **MPD server** (port 6600) | `start_servers()` via `thread::spawn` | `tokio::Builder::new_current_thread` |
111111+| **MPRIS server** (Linux, D-Bus) | `start_servers()` via `thread::spawn` | `async_std` task |
112112+| **UPnP runtime** | `rockbox_upnp::init()` (static `OnceLock`) | `tokio::runtime::Runtime::new()` (multi-thread) |
113113+| **Device scanners** (Chromecast, AirPlay, Snapcast, UPnP, Squeezelite) | `run_http_server()` via `thread::spawn` | each creates its own `tokio::runtime::Runtime::new()` |
114114+| **Player event listener** | `run_http_server()` via `thread::spawn` | `tokio::runtime::Runtime::new()` (multi-thread) |
115115+| **Command relay** | `start_servers()` via `thread::spawn` | `reqwest::blocking` (creates its own tokio internally) |
116116+117117+---
118118+119119+## Startup Sequence
120120+121121+```
122122+main.c: server_init()
123123+│
124124+├── create_thread(server_thread) ← Rockbox kernel thread
125125+│ └── start_server()
126126+│ ├── load_settings()
127127+│ ├── rockbox_upnp::init() ← creates static UPnP multi-thread runtime
128128+│ ├── thread::spawn(actix) ← Rust OS thread (free to block)
129129+│ └── loop { sleep + rb::system::sleep(HZ) } ← yields Rockbox mutex
130130+│
131131+├── sleep(HZ) ← Rockbox scheduler sleep on main thread (~1 s)
132132+│
133133+└── start_servers() ← called on main C thread after 1 s
134134+ ├── thread::spawn(gRPC server) ← Rust OS thread, new_current_thread runtime
135135+ ├── thread::spawn(GraphQL server) ← Rust OS thread, new_current_thread runtime
136136+ ├── thread::spawn(MPD server) ← Rust OS thread, new_current_thread runtime
137137+ ├── thread::spawn(MPRIS, Linux) ← Rust OS thread, async_std
138138+ └── thread::spawn(command relay) ← Rust OS thread, reqwest blocking
139139+140140+main.c: broker_init()
141141+│
142142+└── create_thread(broker_thread) ← Rockbox kernel thread
143143+ └── start_broker()
144144+ └── loop { work + sleep + rb::system::sleep(HZ) }
145145+```
146146+147147+---
148148+149149+## Tokio Runtime Layout
150150+151151+Multiple independent tokio runtimes coexist; they do not share thread pools or event loops.
152152+153153+```
154154+┌─────────────────────────────────────────────────────┐
155155+│ UPnP Runtime (static, multi-thread) │
156156+│ Owns: mDNS/SSDP, UPnP HTTP, UPnP renderer │
157157+└─────────────────────────────────────────────────────┘
158158+159159+┌─────────────────────────────────────────────────────┐
160160+│ actix-rt System (current-thread + LocalSet) │
161161+│ Owns: HTTP REST API handlers (port 6063) │
162162+│ Workers: actix arbiters, each on their own thread │
163163+└─────────────────────────────────────────────────────┘
164164+165165+┌─────────────────────────────────────────────────────┐
166166+│ gRPC Runtime (current-thread) │
167167+│ Owns: tonic server (port 6061) │
168168+└─────────────────────────────────────────────────────┘
169169+170170+┌─────────────────────────────────────────────────────┐
171171+│ GraphQL Runtime (current-thread) │
172172+│ Owns: async-graphql + subscriptions (port 6062) │
173173+└─────────────────────────────────────────────────────┘
174174+175175+┌─────────────────────────────────────────────────────┐
176176+│ MPD Runtime (current-thread) │
177177+│ Owns: MPD protocol server (port 6600) │
178178+└─────────────────────────────────────────────────────┘
179179+180180+┌─────────────────────────────────────────────────────┐
181181+│ Scanner / player-event runtimes (multi-thread each)│
182182+│ Short-lived, one per background scanning thread │
183183+└─────────────────────────────────────────────────────┘
184184+```
185185+186186+All runtimes share a single **SQLite database** (`~/.config/rockbox.org/rockbox-library.db`). The connection pool is configured with:
187187+- WAL journal mode (concurrent readers + one writer)
188188+- `busy_timeout = 30 s` (serialize concurrent writers instead of failing)
189189+190190+---
191191+192192+## Rules & Pitfalls
193193+194194+### Rule 1: Never do a plain OS block inside a Rockbox kernel thread
195195+196196+Wrong — starves every other Rockbox thread:
197197+```rust
198198+pub extern "C" fn start_server() {
199199+ let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
200200+ rt.block_on(run_server()); // WRONG: holds Rockbox mutex, nothing else runs
201201+}
202202+```
203203+204204+Wrong — same problem:
205205+```rust
206206+ let handle = thread::spawn(|| { run_server() });
207207+ handle.join().unwrap(); // WRONG: OS-level block, Rockbox mutex never released
208208+```
209209+210210+Correct — spawn work out, yield in the Rockbox thread:
211211+```rust
212212+ thread::spawn(|| { run_server() });
213213+ loop {
214214+ thread::sleep(Duration::from_millis(100));
215215+ rb::system::sleep(rb::HZ); // releases Rockbox mutex
216216+ }
217217+```
218218+219219+### Rule 2: Use `actix_rt::System` for the HTTP server, not a raw tokio runtime
220220+221221+Wrong — actix detects "existing Tokio runtime" and collapses all workers onto one thread:
222222+```rust
223223+let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
224224+rt.block_on(HttpServer::new(...).run()); // "starting in existing Tokio runtime"
225225+```
226226+227227+Correct:
228228+```rust
229229+actix_rt::System::new().block_on(run_http_server());
230230+```
231231+232232+### Rule 3: Never call `tokio::runtime::Runtime::new()` from inside an active `block_on`
233233+234234+Calling `Runtime::new()` from within a `block_on` context (e.g. inside an async actix handler) panics in tokio 1.27+. This is why `rockbox_upnp::init()` is called **before** any runtime is started in `start_server()`.
235235+236236+### Rule 4: `reqwest::blocking` creates its own tokio runtime internally
237237+238238+The command-relay thread in `start_servers()` uses `reqwest::blocking::Client`. This internally creates a multi-thread tokio runtime. It must run in a plain Rust OS thread, never inside an existing async context.
239239+240240+### Rule 5: `SimpleBroker` is runtime-agnostic
241241+242242+`rockbox_graphql::simplebroker::SimpleBroker` uses `futures_channel::mpsc::UnboundedSender` for pub/sub. `publish()` is a synchronous call — it does not require or interact with any tokio runtime. It is safe to call from any thread, including Rockbox kernel threads and scanner threads with their own runtimes.