The Pedigree Project 0.1
ThreadedIrqDispatcher.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include "pedigree/kernel/ActivityDiagnostics.h"
9#include "pedigree/kernel/Log.h"
10#include "pedigree/kernel/machine/IrqManager.h"
11#include "pedigree/kernel/machine/ThreadedIrqDispatcher.h"
12#include "pedigree/kernel/process/PerProcessorScheduler.h"
13#include "pedigree/kernel/process/Scheduler.h"
14#include "pedigree/kernel/process/TerminationDeferral.h"
15#include "pedigree/kernel/process/Thread.h"
16#include "pedigree/kernel/processor/Processor.h"
17#include "pedigree/kernel/processor/ProcessorInformation.h"
18#include "pedigree/kernel/time/Time.h"
19
20static_assert(__atomic_always_lock_free(sizeof(size_t), nullptr),
21 "IRQ doorbell words must be lock-free");
22static_assert(__atomic_always_lock_free(sizeof(uintptr_t), nullptr),
23 "IRQ diagnostic identities must be lock-free");
24static_assert(__atomic_always_lock_free(sizeof(Thread::DebugState), nullptr),
25 "IRQ worker debug state must be lock-free");
26
27namespace {
28size_t elapsedSince(size_t now, size_t then) {
29 return then && now >= then ? now - then : 0;
30}
31
32void updateMaximum(size_t& maximum, size_t value) {
33 if (value > __atomic_load_n(&maximum, __ATOMIC_RELAXED)) {
34 // Each physical line has exactly one worker updating its maxima.
35 __atomic_store_n(&maximum, value, __ATOMIC_RELEASE);
36 }
37}
38
39IrqWorkerDebugState workerDebugState(Thread::DebugState state) {
40 switch (state) {
41 case Thread::None:
42 return IrqWorkerDebugState::None;
43 case Thread::SemWait:
44 return IrqWorkerDebugState::SemaphoreWait;
45 case Thread::CondWait:
46 return IrqWorkerDebugState::ConditionWait;
47 case Thread::Joining:
48 return IrqWorkerDebugState::Joining;
49 case Thread::FutexWait:
50 return IrqWorkerDebugState::FutexWait;
51 case Thread::EventWait:
52 return IrqWorkerDebugState::EventWait;
53 case Thread::ProcessWait:
54 return IrqWorkerDebugState::ProcessWait;
55 case Thread::CallbackDrain:
56 return IrqWorkerDebugState::CallbackDrain;
57 }
58 return IrqWorkerDebugState::Unavailable;
59}
60
61IrqWorkerWaitReason workerWaitReason(WaitQueue::WakeReason reason) {
62 switch (reason) {
63 case WaitQueue::WakeReason::Waiting:
64 return IrqWorkerWaitReason::Waiting;
65 case WaitQueue::WakeReason::Signalled:
66 return IrqWorkerWaitReason::Signalled;
67 case WaitQueue::WakeReason::Event:
68 return IrqWorkerWaitReason::Event;
69 case WaitQueue::WakeReason::Unwinding:
70 return IrqWorkerWaitReason::Unwinding;
71 case WaitQueue::WakeReason::Terminating:
72 return IrqWorkerWaitReason::Terminating;
73 case WaitQueue::WakeReason::Spurious:
74 return IrqWorkerWaitReason::Spurious;
75 }
76 return IrqWorkerWaitReason::Unavailable;
77}
78} // namespace
79
80ThreadedIrqDispatcher::Line::Line()
81 : m_Owner(nullptr),
82 m_Callback(nullptr),
83 m_CallbackContext(nullptr),
84 m_Thread(nullptr),
85 m_Scheduler(nullptr),
86 m_WorkerWaiters(),
87 m_WorkerWake(),
88 m_WorkerProcessor(0),
89 m_Line(0),
90 m_PendingCookies(nullptr),
91 m_PendingCookieCount(0),
92 m_ActiveCookie(0),
93 m_CallbackActive(0),
94 m_PublicationState(PublicationClosed),
95 m_Started(0),
96 m_CompletedBatches(0),
97 m_CompletedCookie(0),
98 m_PendingSinceTimestamp(0),
99 m_ActiveCallbackStartedTimestamp(0),
100 m_LastWakeLatency(0),
101 m_MaximumWakeLatency(0),
102 m_LastCallbackRuntime(0),
103 m_MaximumCallbackRuntime(0) {}
104
105ThreadedIrqDispatcher::Line::~Line() {
106 if (__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE) ||
107 __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE) || m_PendingCookies) {
108 FATAL("A threaded IRQ worker was destroyed while active.");
109 }
110}
111
112void ThreadedIrqDispatcher::Line::configure(ThreadedIrqDispatcher* owner, uint8_t line,
113 DispatchCallback callback, void* callbackContext) {
114 m_Owner = owner;
115 m_Line = line;
116 m_Callback = callback;
117 m_CallbackContext = callbackContext;
118}
119
120bool ThreadedIrqDispatcher::Line::start() {
121#if THREADS
122 if (__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE) ||
123 __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE) || m_PendingCookies || !m_Owner || !m_Callback) {
124 return false;
125 }
126
127 size_t pendingCookieCount = Processor::getCount();
128#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
129 const size_t testPendingCookieCount =
130 __atomic_load_n(&m_Owner->m_PendingSlotCountForTest, __ATOMIC_ACQUIRE);
131 if (testPendingCookieCount) {
132 pendingCookieCount = testPendingCookieCount;
133 }
134#endif
135 if (!pendingCookieCount) {
136 return false;
137 }
138 size_t* pendingCookies = new size_t[pendingCookieCount];
139 for (size_t i = 0; i < pendingCookieCount; ++i) {
140 __atomic_store_n(&pendingCookies[i], static_cast<size_t>(0), __ATOMIC_RELAXED);
141 }
142 m_PendingCookies = pendingCookies;
143 m_PendingCookieCount = pendingCookieCount;
144 __atomic_store_n(&m_ActiveCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
145 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
146 __atomic_store_n(&m_PublicationState, static_cast<size_t>(0), __ATOMIC_RELEASE);
147 __atomic_store_n(&m_CompletedBatches, static_cast<size_t>(0), __ATOMIC_RELEASE);
148 __atomic_store_n(&m_CompletedCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
149 __atomic_store_n(&m_PendingSinceTimestamp, static_cast<size_t>(0), __ATOMIC_RELEASE);
150 __atomic_store_n(&m_ActiveCallbackStartedTimestamp, static_cast<size_t>(0), __ATOMIC_RELEASE);
151 __atomic_store_n(&m_LastWakeLatency, static_cast<size_t>(0), __ATOMIC_RELEASE);
152 __atomic_store_n(&m_MaximumWakeLatency, static_cast<size_t>(0), __ATOMIC_RELEASE);
153 __atomic_store_n(&m_LastCallbackRuntime, static_cast<size_t>(0), __ATOMIC_RELEASE);
154 __atomic_store_n(&m_MaximumCallbackRuntime, static_cast<size_t>(0), __ATOMIC_RELEASE);
155
156 m_Scheduler = &Processor::information().getScheduler();
157 // The worker cannot migrate between scheduler instances. Capture the
158 // topology index once so a remote hard producer can request an immediate
159 // reschedule of this exact scheduler rather than waiting for its next
160 // periodic timer interrupt.
161 m_WorkerProcessor = Processor::index();
162 Thread* thread = new Thread(Scheduler::instance().getKernelProcess(), workerEntry, this, nullptr,
163 false, true, true);
164 __atomic_store_n(&m_Thread, thread, __ATOMIC_RELEASE);
165 const String workerName(static_cast<const char*>(m_Owner->m_Name), m_Owner->m_Name.length());
166 thread->setName(workerName);
167 m_Scheduler->registerWorkerWake(m_WorkerWake, m_WorkerWaiters);
168
169 __atomic_store_n(&m_Started, static_cast<size_t>(1), __ATOMIC_RELEASE);
170 if (!thread->start()) {
171 // This can only fail if a freshly-created delayed Thread has already
172 // entered an impossible lifecycle state. Continuing would strand a
173 // registered kernel Thread which cannot be safely reclaimed here.
174 FATAL("A threaded IRQ worker could not be started.");
175 return false;
176 }
177 return true;
178#else
179 return false;
180#endif
181}
182
183void ThreadedIrqDispatcher::Line::beginStop() {
184 if (!__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE)) {
185 return;
186 }
187 // One atomic word closes admission and counts publishers already inside
188 // publishFromInterrupt(). The worker does not exit until that count drains.
189 __atomic_fetch_or(&m_PublicationState, PublicationClosed, __ATOMIC_ACQ_REL);
190 m_Scheduler->ringIrqWorkDoorbell(m_WorkerWake);
191}
192
193bool ThreadedIrqDispatcher::Line::join() {
194 if (!__atomic_load_n(&m_Started, __ATOMIC_ACQUIRE)) {
195 return __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE) == nullptr;
196 }
197
198 Thread* thread = __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE);
199 if (!thread || !thread->joinForCompletion()) {
200 return false;
201 }
202
203 if (m_Scheduler) {
204 m_Scheduler->unregisterWorkerWake(m_WorkerWake);
205 }
206 __atomic_store_n(&m_Thread, static_cast<Thread*>(nullptr), __ATOMIC_RELEASE);
207 m_Scheduler = nullptr;
208 __atomic_store_n(&m_Started, static_cast<size_t>(0), __ATOMIC_RELEASE);
209 delete[] m_PendingCookies;
210 m_PendingCookies = nullptr;
211 m_PendingCookieCount = 0;
212 __atomic_store_n(&m_ActiveCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
213 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
214 return true;
215}
216
217bool ThreadedIrqDispatcher::Line::publishFromInterrupt(size_t cookie) {
218 if (!cookie) {
219 return false;
220 }
221
222 size_t processor = Processor::index();
223#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
224 const size_t processorSlot =
225 __atomic_load_n(&m_Owner->m_PublicationSlotForTest, __ATOMIC_ACQUIRE);
226 if (processorSlot != static_cast<size_t>(-1)) {
227 processor = processorSlot;
228 }
229#endif
230
231 const bool remoteProducer = processor != m_WorkerProcessor;
232 if (remoteProducer && !m_Owner->m_RemoteWakeCallback) {
233#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
234 __atomic_add_fetch(&m_Owner->m_RemotePublicationRejectionsForTest, static_cast<size_t>(1),
235 __ATOMIC_RELAXED);
236#endif
237 // A local-only dispatcher cannot safely leave remote work dependent
238 // on a future unrelated timer tick. Reject before publication so the
239 // caller's fail-closed policy retains the terminal IRQ obligations.
240 return false;
241 }
242
243 const size_t admission =
244 __atomic_fetch_add(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
245 if (admission & PublicationClosed) {
246 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
247 return false;
248 }
249
250 if (!m_PendingCookies || processor >= m_PendingCookieCount) {
251 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
252 // Processor topology is fixed before dispatcher initialisation. A
253 // failure here is therefore a lifecycle/configuration rejection, not
254 // a contention fallback which can lose an admitted edge.
255 return false;
256 }
257
258 // Maskable hard interrupts cannot run concurrently on one processor.
259 // Per-processor slots make publication one wait-free exchange. A nested
260 // publication happens after this store and may replace it with a later
261 // generation; the outer publisher never writes again when it resumes.
262 const size_t pending =
263 __atomic_exchange_n(&m_PendingCookies[processor], cookie, __ATOMIC_ACQ_REL);
264 if (!pending) {
265 __atomic_store_n(&m_PendingSinceTimestamp, static_cast<size_t>(Time::getTicks()),
266 __ATOMIC_RELEASE);
267 }
268
269#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
270 PublicationObservedHook hook =
271 __atomic_load_n(&m_Owner->m_PublicationObservedHook, __ATOMIC_ACQUIRE);
272 if (hook) {
273 hook(m_Owner, m_Line, cookie, pending);
274 }
275#endif
276
277 // The worker is pinned to this scheduler. Stage its local doorbell before
278 // issuing a directed prompt so a fast IPI observes the published cookie. A
279 // 0-to-pending transition is the sole prompt obligation; later occurrences
280 // coalesce into the batch the first prompt exposed.
281 m_Scheduler->ringIrqWorkDoorbell(m_WorkerWake);
282 if (remoteProducer && !pending) {
283 if (!m_Owner->m_RemoteWakeCallback(m_Owner->m_RemoteWakeCallbackContext, m_Line,
284 m_WorkerProcessor)) {
285 // The occurrence is already accepted and visible. Rolling it
286 // back would strand the owning controller's acknowledgement or
287 // mask state, so a failed directed wake is terminally explicit.
288 FATAL_NOLOCK("Threaded IRQ remote worker prompt failed after publication.");
289 }
290 }
291
292 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
293 return true;
294}
295
296bool ThreadedIrqDispatcher::Line::hasPending() const {
297 return pendingCookie() != 0;
298}
299
300bool ThreadedIrqDispatcher::Line::isWorker(const Thread* thread) const {
301 return thread && thread == __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE);
302}
303
304size_t ThreadedIrqDispatcher::Line::pendingCookie() const {
305 // External diagnostics can race orderly dispatcher shutdown. Share the
306 // publisher admission word so join cannot release the slot array while a
307 // detached scan is in progress.
308 const size_t admission =
309 __atomic_fetch_add(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
310 if (admission & PublicationClosed) {
311 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
312 return 0;
313 }
314
315#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
316 PendingScanAdmittedHook hook =
317 __atomic_load_n(&m_Owner->m_PendingScanAdmittedHook, __ATOMIC_ACQUIRE);
318 if (hook) {
319 hook(m_Owner, m_Line);
320 }
321#endif
322
323 const size_t pending = pendingCookieForWorker();
324 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
325 return pending;
326}
327
328size_t ThreadedIrqDispatcher::Line::pendingCookieForWorker() const {
329 size_t newest = 0;
330 for (size_t i = 0; i < m_PendingCookieCount; ++i) {
331 const size_t candidate = __atomic_load_n(&m_PendingCookies[i], __ATOMIC_ACQUIRE);
332 if (candidate && (!newest || generationReached(candidate, newest))) {
333 newest = candidate;
334 }
335 }
336 return newest;
337}
338
339size_t ThreadedIrqDispatcher::Line::activeCookie() const {
340 return __atomic_load_n(&m_ActiveCookie, __ATOMIC_ACQUIRE);
341}
342
343size_t ThreadedIrqDispatcher::Line::completedBatches() const {
344 return __atomic_load_n(&m_CompletedBatches, __ATOMIC_ACQUIRE);
345}
346
347size_t ThreadedIrqDispatcher::Line::completedCookie() const {
348 return __atomic_load_n(&m_CompletedCookie, __ATOMIC_ACQUIRE);
349}
350
351uintptr_t ThreadedIrqDispatcher::Line::workerIdentity() const {
352 return reinterpret_cast<uintptr_t>(__atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE));
353}
354
355bool ThreadedIrqDispatcher::Line::callbackActive() const {
356 return __atomic_load_n(&m_CallbackActive, __ATOMIC_ACQUIRE) != 0;
357}
358
359bool ThreadedIrqDispatcher::Line::publicationClosed() const {
360 return (__atomic_load_n(&m_PublicationState, __ATOMIC_ACQUIRE) & PublicationClosed) != 0;
361}
362
363void ThreadedIrqDispatcher::Line::snapshotDiagnostics(IrqLineDiagnosticSnapshot& snapshot) const {
364 snapshot.workerDiagnosticAvailable = false;
365 snapshot.workerDebugState = IrqWorkerDebugState::Unavailable;
366 snapshot.workerDebugAddress = 0;
367 snapshot.workerWaitActive = false;
368 snapshot.workerWaitQueue = 0;
369 snapshot.workerWaitChannelOwner = 0;
370 snapshot.workerWaitChannelValue = 0;
371 snapshot.workerWaitReason = IrqWorkerWaitReason::Unavailable;
372 snapshot.workerWaitStateLevel = 0;
373 snapshot.workerWaitQueued = false;
374 snapshot.observationTimestamp = static_cast<size_t>(Time::getTicks());
375 snapshot.pendingSinceTimestamp = __atomic_load_n(&m_PendingSinceTimestamp, __ATOMIC_ACQUIRE);
376 snapshot.activeCallbackStartedTimestamp =
377 __atomic_load_n(&m_ActiveCallbackStartedTimestamp, __ATOMIC_ACQUIRE);
378 snapshot.lastWakeLatency = __atomic_load_n(&m_LastWakeLatency, __ATOMIC_ACQUIRE);
379 snapshot.maximumWakeLatency = __atomic_load_n(&m_MaximumWakeLatency, __ATOMIC_ACQUIRE);
380 snapshot.lastCallbackRuntime = __atomic_load_n(&m_LastCallbackRuntime, __ATOMIC_ACQUIRE);
381 snapshot.maximumCallbackRuntime = __atomic_load_n(&m_MaximumCallbackRuntime, __ATOMIC_ACQUIRE);
382
383 // Shutdown closes this shared admission word before joining the worker.
384 // A diagnostic reader admitted here therefore pins m_Thread until its
385 // detached copy is complete without taking a lock which the debugger may
386 // have interrupted another CPU while holding.
387 const size_t admission =
388 __atomic_fetch_add(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
389 if (admission & PublicationClosed) {
390 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
391 return;
392 }
393
394 Thread* thread = __atomic_load_n(&m_Thread, __ATOMIC_ACQUIRE);
395 if (thread) {
396 snapshot.workerDiagnosticAvailable = true;
397 uintptr_t debugAddress = 0;
398 snapshot.workerDebugState = workerDebugState(thread->getDebugState(debugAddress));
399 snapshot.workerDebugAddress = debugAddress;
400
401 Thread::WaitDebugInfo wait = {};
402 if (thread->getWaitDebugInfo(wait)) {
403 snapshot.workerWaitActive = true;
404 snapshot.workerWaitQueue = reinterpret_cast<uintptr_t>(wait.queue);
405 snapshot.workerWaitChannelOwner = reinterpret_cast<uintptr_t>(wait.channelOwner);
406 snapshot.workerWaitChannelValue = wait.channelValue;
407 snapshot.workerWaitReason = workerWaitReason(wait.reason);
408 snapshot.workerWaitStateLevel = wait.stateLevel;
409 snapshot.workerWaitQueued = wait.queued;
410 }
411 }
412
413 __atomic_fetch_sub(&m_PublicationState, static_cast<size_t>(1), __ATOMIC_RELEASE);
414}
415
416int ThreadedIrqDispatcher::Line::workerEntry(void* context) {
417 return reinterpret_cast<Line*>(context)->run();
418}
419
420int ThreadedIrqDispatcher::Line::run() {
421 // Manager-owned workers are retired only by shutdown(). A terminal
422 // request must not strand a line which still accepts publications.
423 TerminationDeferral workerLifetime;
424 while (true) {
425 // Keep callback state visible for diagnostics while this worker owns a
426 // claimed batch. It is already Running/Ready in the scheduler, so no
427 // scheduler-side predicate is needed to protect this interval.
428 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(1), __ATOMIC_RELEASE);
429 const size_t pendingSince = __atomic_load_n(&m_PendingSinceTimestamp, __ATOMIC_ACQUIRE);
430 const size_t cookie = takePendingCookie();
431 if (cookie) {
432 const size_t completedCookie = __atomic_load_n(&m_CompletedCookie, __ATOMIC_ACQUIRE);
433 if (completedCookie && !generationReached(cookie, completedCookie)) {
434 // A cross-CPU scan can claim an older slot after another
435 // batch has already completed. The delivered high-water
436 // suppresses that stale generation without suppressing
437 // equal-cookie work-bit publications.
438 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
440 continue;
441 }
442
443 const size_t started = static_cast<size_t>(Time::getTicks());
444 const size_t wakeLatency = elapsedSince(started, pendingSince);
445 __atomic_store_n(&m_LastWakeLatency, wakeLatency, __ATOMIC_RELEASE);
446 updateMaximum(m_MaximumWakeLatency, wakeLatency);
447 __atomic_store_n(&m_ActiveCallbackStartedTimestamp, started, __ATOMIC_RELEASE);
448 __atomic_store_n(&m_ActiveCookie, cookie, __ATOMIC_RELEASE);
449 m_Callback(m_CallbackContext, m_Line, cookie);
450 const size_t completed = static_cast<size_t>(Time::getTicks());
451 const size_t runtime = elapsedSince(completed, started);
452 ActivityDiagnostics::recordThreadedDispatch(m_Line, runtime);
453 __atomic_store_n(&m_LastCallbackRuntime, runtime, __ATOMIC_RELEASE);
454 updateMaximum(m_MaximumCallbackRuntime, runtime);
455 __atomic_add_fetch(&m_CompletedBatches, static_cast<size_t>(1), __ATOMIC_ACQ_REL);
456 __atomic_store_n(&m_CompletedCookie, cookie, __ATOMIC_RELEASE);
457 __atomic_store_n(&m_ActiveCookie, static_cast<size_t>(0), __ATOMIC_RELEASE);
458 __atomic_store_n(&m_ActiveCallbackStartedTimestamp, static_cast<size_t>(0), __ATOMIC_RELEASE);
459 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
460 // A continuously asserted source must not turn its threaded
461 // bottom half into an unbounded softirq loop. One completed batch
462 // is the scheduling budget before ordinary peers get a chance.
464 continue;
465 }
466
467 __atomic_store_n(&m_CallbackActive, static_cast<size_t>(0), __ATOMIC_RELEASE);
468 const size_t publicationState = __atomic_load_n(&m_PublicationState, __ATOMIC_ACQUIRE);
469 if (publicationState & PublicationClosed) {
470 if (publicationState & PublicationCountMask) {
472 continue;
473 }
474
475 // An admitted publisher stores its cookie before dropping the
476 // final count. Recheck after observing zero so close cannot race
477 // the worker past an already-accepted occurrence.
478 if (!hasPendingForWorker()) {
479 break;
480 }
481 continue;
482 }
483
484 // Recheck the publication state while holding the wait queue's lock. A
485 // producer which races this check either finds a waiter to wake or leaves
486 // its published cookie visible for this second check.
487 auto guard = m_WorkerWaiters.acquire();
488 const size_t currentPublicationState = __atomic_load_n(&m_PublicationState, __ATOMIC_ACQUIRE);
489 if (!hasPendingForWorker() && !(currentPublicationState & PublicationClosed)) {
490 const WaitQueue::WakeReason reason =
491 guard.wait(WaitQueue::Channel(), Thread::CondWait, reinterpret_cast<uintptr_t>(this));
492 (void)reason;
493 }
494 }
495
496 return 0;
497}
498
499bool ThreadedIrqDispatcher::Line::hasPendingForWorker() const {
500 return pendingCookieForWorker() != 0;
501}
502
503size_t ThreadedIrqDispatcher::Line::takePendingCookie() {
504 size_t newest = 0;
505 for (size_t i = 0; i < m_PendingCookieCount; ++i) {
506 const size_t candidate =
507 __atomic_exchange_n(&m_PendingCookies[i], static_cast<size_t>(0), __ATOMIC_ACQ_REL);
508 if (candidate && (!newest || generationReached(candidate, newest))) {
509 newest = candidate;
510 }
511 }
512 return newest;
513}
514
515bool ThreadedIrqDispatcher::Line::generationReached(size_t current, size_t target) {
516 // Cookies advance monotonically and no live publication can span half of
517 // the size_t range, so signed modular distance preserves wrap ordering.
518 return static_cast<intptr_t>(current - target) >= 0;
519}
520
521ThreadedIrqDispatcher::ThreadedIrqDispatcher(const String& name, size_t lineCount,
522 DispatchCallback callback, void* callbackContext)
523 : m_Lines(),
524 m_Name(name),
525 m_LineCount(lineCount),
526 m_Callback(callback),
527 m_CallbackContext(callbackContext),
528 m_ConfigurationLock(false),
529 m_RemoteWakeCallback(nullptr),
530 m_RemoteWakeCallbackContext(nullptr),
532 m_Initialised(false),
534#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
535 ,
536 m_PublicationObservedHook(nullptr),
537 m_PendingScanAdmittedHook(nullptr),
538 m_PendingSlotCountForTest(0),
539 m_PublicationSlotForTest(static_cast<size_t>(-1)),
540 m_RejectNextPublicationForTest(0),
541 m_RemotePublicationRejectionsForTest(0)
542#endif
543{
544 if (m_LineCount > MaxLines) {
545 m_LineCount = MaxLines;
546 }
547}
548
550 void* callbackContext) {
552 if (__atomic_load_n(&m_ConfigurationClosed, __ATOMIC_ACQUIRE)) {
554 return false;
555 }
556
557 // This pair is immutable once any worker begins, so hard publication can
558 // read it without another lock or an allocation-dependent indirection.
559 m_RemoteWakeCallback = callback;
560 m_RemoteWakeCallbackContext = callbackContext;
562 return true;
563}
564
565#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
566void ThreadedIrqDispatcher::setPublicationObservedHookForTest(PublicationObservedHook hook) {
567 __atomic_store_n(&m_PublicationObservedHook, hook, __ATOMIC_RELEASE);
568}
569
570bool ThreadedIrqDispatcher::setPendingSlotCountForTest(size_t slotCount) {
571 if (isInitialised() || !slotCount) {
572 return false;
573 }
574
575 __atomic_store_n(&m_PendingSlotCountForTest, slotCount, __ATOMIC_RELEASE);
576 return true;
577}
578
579bool ThreadedIrqDispatcher::publishFromSlotForTest(uint8_t line, size_t slot, size_t cookie) {
580 if (!isInitialised() || line >= m_LineCount) {
581 return false;
582 }
583
584 size_t expected = static_cast<size_t>(-1);
585 if (!__atomic_compare_exchange_n(&m_PublicationSlotForTest, &expected, slot, false,
586 __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
587 return false;
588 }
589 const bool published = m_Lines[line].publishFromInterrupt(cookie);
590 __atomic_store_n(&m_PublicationSlotForTest, static_cast<size_t>(-1), __ATOMIC_RELEASE);
591 return published;
592}
593
594void ThreadedIrqDispatcher::rejectNextPublicationForTest() {
595 __atomic_store_n(&m_RejectNextPublicationForTest, static_cast<size_t>(1), __ATOMIC_RELEASE);
596}
597
598void ThreadedIrqDispatcher::setPendingScanAdmittedHookForTest(PendingScanAdmittedHook hook) {
599 __atomic_store_n(&m_PendingScanAdmittedHook, hook, __ATOMIC_RELEASE);
600}
601
602#endif
603
604ThreadedIrqDispatcher::~ThreadedIrqDispatcher() {
605 if (__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE)) {
606 FATAL("Threaded IRQ dispatcher was destroyed before shutdown.");
607 }
608}
609
611#if THREADS
612 if (__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE) || !m_LineCount || !m_Callback) {
613 return false;
614 }
615
617 size_t configurationExpected = 0;
618 if (!__atomic_compare_exchange_n(&m_ConfigurationClosed, &configurationExpected,
619 static_cast<size_t>(1), false, __ATOMIC_ACQ_REL,
620 __ATOMIC_ACQUIRE)) {
622 return false;
623 }
625
626#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
627 __atomic_store_n(&m_RejectNextPublicationForTest, static_cast<size_t>(0), __ATOMIC_RELEASE);
628 __atomic_store_n(&m_RemotePublicationRejectionsForTest, static_cast<size_t>(0), __ATOMIC_RELEASE);
629#endif
630 __atomic_store_n(&m_ShutdownClaimed, static_cast<size_t>(0), __ATOMIC_RELEASE);
631
632 for (size_t i = 0; i < m_LineCount; ++i) {
633 m_Lines[i].configure(this, static_cast<uint8_t>(i), m_Callback, m_CallbackContext);
634 if (!m_Lines[i].start()) {
635 for (size_t j = 0; j < i; ++j) {
636 m_Lines[j].beginStop();
637 }
638 Processor::information().getScheduler().serviceIrqWorkDoorbell();
639 for (size_t j = 0; j < i; ++j) {
640 m_Lines[j].join();
641 }
642 __atomic_store_n(&m_ConfigurationClosed, static_cast<size_t>(0), __ATOMIC_RELEASE);
643 return false;
644 }
645 }
646
647 __atomic_store_n(&m_Initialised, static_cast<size_t>(1), __ATOMIC_RELEASE);
648 return true;
649#else
650 return true;
651#endif
652}
653
655#if THREADS
656 if (!__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE)) {
657 return true;
658 }
659 if (!canShutdown()) {
660 return false;
661 }
662 size_t expected = 0;
663 if (!__atomic_compare_exchange_n(&m_ShutdownClaimed, &expected, static_cast<size_t>(1), false,
664 __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
665 // A successful claimant owns every join and slot release. A racing
666 // caller must not join the same workers or free the same arrays.
667 return !__atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE);
668 }
669
670 for (size_t i = 0; i < m_LineCount; ++i) {
671 m_Lines[i].beginStop();
672 }
673
674 Processor::information().getScheduler().serviceIrqWorkDoorbell();
675
676 bool joined = true;
677 for (size_t i = 0; i < m_LineCount; ++i) {
678 joined &= m_Lines[i].join();
679 }
680 if (joined) {
681 __atomic_store_n(&m_Initialised, static_cast<size_t>(0), __ATOMIC_RELEASE);
682 __atomic_store_n(&m_ConfigurationClosed, static_cast<size_t>(0), __ATOMIC_RELEASE);
683 } else {
684 // Successfully joined lines are already inert. Let a later ordinary
685 // thread retry only the workers which did not complete this drain.
686 __atomic_store_n(&m_ShutdownClaimed, static_cast<size_t>(0), __ATOMIC_RELEASE);
687 }
688 return joined;
689#else
690 return true;
691#endif
692}
693
695 Thread* current = Processor::information().getCurrentThread();
696 bool safe = current && Processor::executionContext() == ExecutionContext::WaitableThread &&
698#if HOSTED
699 safe = safe && !current->getHostedSignalDepth();
700#endif
701 return safe;
702}
703
704bool ThreadedIrqDispatcher::isInitialised() const {
705 return __atomic_load_n(&m_Initialised, __ATOMIC_ACQUIRE) != 0;
706}
707
709#if THREADS
710 const Thread* current = Processor::information().getCurrentThread();
711 for (size_t i = 0; i < m_LineCount; ++i) {
712 if (m_Lines[i].isWorker(current)) {
713 return true;
714 }
715 }
716#endif
717 return false;
718}
719
720bool ThreadedIrqDispatcher::publishFromInterrupt(uint8_t line, size_t cookie) {
721 if (!isInitialised() || line >= m_LineCount || !cookie) {
722 return false;
723 }
724#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
725 if (__atomic_exchange_n(&m_RejectNextPublicationForTest, static_cast<size_t>(0),
726 __ATOMIC_ACQ_REL)) {
727 return false;
728 }
729#endif
730 return m_Lines[line].publishFromInterrupt(cookie);
731}
732
733bool ThreadedIrqDispatcher::hasPending(uint8_t line) const {
734 return isInitialised() && line < m_LineCount && m_Lines[line].hasPending();
735}
736
737size_t ThreadedIrqDispatcher::pendingCookie(uint8_t line) const {
738 return line < m_LineCount ? m_Lines[line].pendingCookie() : 0;
739}
740
741size_t ThreadedIrqDispatcher::activeCookie(uint8_t line) const {
742 return line < m_LineCount ? m_Lines[line].activeCookie() : 0;
743}
744
745size_t ThreadedIrqDispatcher::completedBatches(uint8_t line) const {
746 return line < m_LineCount ? m_Lines[line].completedBatches() : 0;
747}
748
749size_t ThreadedIrqDispatcher::completedCookie(uint8_t line) const {
750 return line < m_LineCount ? m_Lines[line].completedCookie() : 0;
751}
752
753uintptr_t ThreadedIrqDispatcher::workerIdentity(uint8_t line) const {
754 return line < m_LineCount ? m_Lines[line].workerIdentity() : 0;
755}
756
757bool ThreadedIrqDispatcher::callbackActive(uint8_t line) const {
758 return line < m_LineCount && m_Lines[line].callbackActive();
759}
760
761bool ThreadedIrqDispatcher::publicationClosed(uint8_t line) const {
762 return line < m_LineCount && m_Lines[line].publicationClosed();
763}
764
766 IrqLineDiagnosticSnapshot& snapshot) const {
767 if (line < m_LineCount) {
768 m_Lines[line].snapshotDiagnostics(snapshot);
769 }
770}
static ProcessorInformation & information()
static size_t getCount()
static ExecutionContext executionContext()
Definition Processor.cc:109
static size_t index()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
void release()
Definition Spinlock.cc:161
bool acquire(bool recurse=false, bool safe=true)
Definition Spinlock.cc:35
DebugState
Definition Thread.h:176
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3184
bool joinForCompletion()
Definition Thread.cc:2771
DebugState getDebugState(uintptr_t &address)
Definition Thread.h:570
bool start()
Definition Thread.cc:794
bool hasPending(uint8_t line) const
size_t pendingCookie(uint8_t line) const
RemoteWakeCallback m_RemoteWakeCallback
bool(*)(void *, uint8_t, size_t) RemoteWakeCallback
bool publishFromInterrupt(uint8_t line, size_t cookie)
MUST_USE_RESULT bool setRemoteWakeCallback(RemoteWakeCallback callback, void *callbackContext)
void snapshotDiagnostics(uint8_t line, IrqLineDiagnosticSnapshot &snapshot) const