The Pedigree Project 0.1
NvmeQueue.cc
1/* Copyright (c) 2026, Pedigree Developers. SPDX-License-Identifier: ISC */
2#include "NvmeQueue.h"
3#include "pedigree/kernel/LockGuard.h"
4#include "pedigree/kernel/Log.h"
5#include "pedigree/kernel/TargetInfo.h"
6#include "pedigree/kernel/process/TerminationDeferral.h"
7#include "pedigree/kernel/processor/IoBase.h"
8#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
9#include "pedigree/kernel/processor/Processor.h"
10#include "pedigree/kernel/processor/VirtualAddressSpace.h"
11#include "pedigree/kernel/time/Time.h"
12#include "pedigree/kernel/utilities/utility.h"
13
14NvmeQueue::Slot::Slot()
15 : data("NVMe transfer"),
16 prps("NVMe PRP list"),
17 completion(0, false),
18 active(false),
19 done(false),
20 status(0),
21 result(0) {}
22NvmeQueue::NvmeQueue()
23 : m_Registers(nullptr),
24 m_DmaDevice(nullptr),
25 m_Submission("NVMe SQ"),
26 m_Completion("NVMe CQ"),
27 m_Available(0, false),
28 m_Stride(0),
29 m_TransferBytes(0),
30 m_Id(0),
31 m_Depth(0),
32 m_Tail(0),
33 m_Head(0),
34 m_SubmissionHead(0),
35 m_Phase(true),
36 m_Online(false),
37 m_PolledInterrupt(false),
38 m_InterruptCompletions(0),
39 m_Outstanding(0),
40 m_MaximumOutstanding(0) {}
41
42bool NvmeQueue::initialise(IoBase* registers, uint16_t id, uint16_t depth, size_t stride,
43 size_t transferBytes, Device* dmaDevice) {
44 if (TargetInfo::getPageSize() != Nvme::PageSize || depth < 2 || depth > Nvme::QueueDepth ||
45 !transferBytes || transferBytes > Nvme::MaxTransfer || transferBytes % Nvme::PageSize)
46 return false;
47 m_Registers = registers;
48 m_DmaDevice = dmaDevice;
49 m_Id = id;
50 m_Depth = depth;
51 m_Stride = stride;
52 m_TransferBytes = transferBytes;
53 auto& memory = PhysicalMemoryManager::instance();
56 if (!memory.allocateRegion(m_Submission, 1, constraints, flags) ||
57 !memory.allocateRegion(m_Completion, 1, constraints, flags))
58 return false;
59 auto& pci = PciBus::instance();
60 if (!pci.mapDmaPage(m_DmaDevice, m_Submission.physicalAddress(), Nvme::PageSize,
61 m_SubmissionDma) ||
62 !pci.mapDmaPage(m_DmaDevice, m_Completion.physicalAddress(), Nvme::PageSize,
63 m_CompletionDma)) {
64 return false;
65 }
66 ByteSet(m_Submission.virtualAddress(), 0, Nvme::PageSize);
67 ByteSet(m_Completion.virtualAddress(), 0, Nvme::PageSize);
68 for (size_t i = 0; i < depth - 1U; ++i) {
69 auto& slot = m_Slots[i];
70 if (!memory.allocateRegion(slot.data, transferBytes / Nvme::PageSize,
72 !memory.allocateRegion(slot.prps, 1, constraints, flags))
73 return false;
74 if (!pci.mapDmaPage(m_DmaDevice, slot.prps.physicalAddress(), Nvme::PageSize, slot.prpsDma)) {
75 return false;
76 }
77 ByteSet(slot.data.virtualAddress(), 0, transferBytes);
78 ByteSet(slot.prps.virtualAddress(), 0, Nvme::PageSize);
79 auto* prps = static_cast<uint64_t*>(slot.prps.virtualAddress());
80 // PRPs do not require adjacent physical pages. Preserve the low DMA pool
81 // for devices whose descriptors really need contiguous allocations.
82 for (size_t page = 0; page < transferBytes / Nvme::PageSize; ++page) {
83 physical_uintptr_t address = 0;
84 size_t mappingFlags = 0;
86 static_cast<uint8_t*>(slot.data.virtualAddress()) + page * Nvme::PageSize, address,
87 mappingFlags);
88 if (!pci.mapDmaPage(m_DmaDevice, address, Nvme::PageSize, slot.dataDma[page])) {
89 return false;
90 }
91 if (!page)
92 slot.firstPage = slot.dataDma[page].address();
93 else
94 prps[page - 1] = slot.dataDma[page].address();
95 }
96 }
97 FENCE();
98 m_Online = true;
99 m_Available.release(depth - 1);
100 return true;
101}
102
103void NvmeQueue::stopLocked() {
104 if (!m_Online)
105 return;
106 m_Online = false;
107 for (size_t i = 0; i < m_Depth - 1U; ++i) {
108 if (m_Slots[i].active)
109 m_Slots[i].completion.release();
110 }
111 m_Available.release(m_Depth);
112}
113void NvmeQueue::stop() {
114 LockGuard<Mutex> guard(m_Lock);
115 stopLocked();
116}
117
119 m_SubmissionDma.release();
120 m_CompletionDma.release();
121 for (auto& slot : m_Slots) {
122 slot.prpsDma.release();
123 for (auto& mapping : slot.dataDma) {
124 mapping.release();
125 }
126 }
127}
128
129bool NvmeQueue::observe(bool fromInterrupt, bool interruptsEnabled) {
130 const bool credited = fromInterrupt && m_PolledInterrupt;
131 if (fromInterrupt)
132 m_PolledInterrupt = false;
133 if (!m_Online)
134 return credited;
135 auto* entries = static_cast<volatile Nvme::Completion*>(m_Completion.virtualAddress());
136 bool consumed = false;
137 for (size_t count = 0; count < m_Depth; ++count) {
138 volatile auto& entry = entries[m_Head];
139 const uint16_t status = entry.status;
140 if ((status & 1U) != m_Phase)
141 break;
142 // The phase bit publishes the remainder of the coherent DMA completion.
143 FENCE();
144 const uint16_t cid = entry.cid;
145 if (entry.sqId != m_Id || entry.sqHead >= m_Depth || cid >= m_Depth - 1U ||
146 !m_Slots[cid].active || m_Slots[cid].done) {
147 ERROR("NVMe: invalid completion on queue " << m_Id);
148 stopLocked();
149 return true;
150 }
151 auto& slot = m_Slots[cid];
152 m_SubmissionHead = entry.sqHead;
153 slot.status = (status >> 1) & 0x7ffU;
154 slot.result = entry.result;
155 slot.done = true;
156 --m_Outstanding;
157 if (fromInterrupt)
158 ++m_InterruptCompletions;
159 slot.completion.release();
160 if (++m_Head == m_Depth) {
161 m_Head = 0;
162 m_Phase = !m_Phase;
163 }
164 consumed = true;
165 }
166 if (consumed) {
167 // Advancing CQ head can withdraw INTx before its IRQ worker observes it.
168 if (!fromInterrupt && interruptsEnabled)
169 m_PolledInterrupt = true;
170 FENCE();
171 m_Registers->write32(m_Head, Nvme::Doorbells + (2U * m_Id + 1U) * m_Stride);
172 (void)m_Registers->read32(Nvme::Status);
173 }
174 return consumed || credited;
175}
176bool NvmeQueue::complete(bool fromInterrupt) {
177 LockGuard<Mutex> guard(m_Lock);
178 return observe(fromInterrupt);
179}
180
181NvmeQueue::Result NvmeQueue::execute(Nvme::Command command, void* buffer, size_t bytes,
182 bool writing, bool interrupts, size_t timeoutSeconds,
183 uint32_t* result, bool interruptProbe, bool cacheFill,
184 physical_uintptr_t directWritePhysical,
185 PciBus::DmaMapping* directReadMapping) {
186 TerminationDeferral lifetime;
187 if (bytes > m_TransferBytes || (bytes && !buffer))
188 return Result::CommandError;
189 const bool directWrite = directWritePhysical != 0;
190 if (directWrite && (!writing || cacheFill || !bytes || bytes > Nvme::PageSize ||
191 (reinterpret_cast<uintptr_t>(buffer) & (Nvme::PageSize - 1)) ||
192 (directWritePhysical & (Nvme::PageSize - 1)) ||
193 uint64_t{directWritePhysical} >= (uint64_t{1} << 32))) {
194 return Result::CommandError;
195 }
196 physical_uintptr_t directPage = 0;
197 bool direct = false;
198 const size_t pageOffset = reinterpret_cast<uintptr_t>(buffer) & (Nvme::PageSize - 1);
199 // PRP1 may have a page offset, but its address must remain DWORD aligned.
200 if (cacheFill && !writing && bytes && !(pageOffset & 3U) &&
201 bytes <= Nvme::PageSize - pageOffset) {
202 size_t flags = 0;
204 physical_uintptr_t physical = 0;
205 void* page = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(buffer) - pageOffset);
206 if (space.getMapping(page, physical, flags) && physical && !(physical & (Nvme::PageSize - 1)) &&
211 if (directReadMapping && PciBus::instance().mapDmaPage(m_DmaDevice, physical, Nvme::PageSize,
212 *directReadMapping)) {
213 directPage = directReadMapping->address() + pageOffset;
214 direct = true;
215 }
216 }
217 }
218 if (!m_Available.acquireForCompletion(1, timeoutSeconds)) {
219 stop();
220 return Result::TransportError;
221 }
222 size_t cid = 0;
223 {
224 LockGuard<Mutex> guard(m_Lock);
225 if (!m_Online) {
226 m_Available.release();
227 return Result::TransportError;
228 }
229 for (; cid < m_Depth - 1U && m_Slots[cid].active; ++cid) {
230 }
231 const uint16_t nextTail = (m_Tail + 1U) % m_Depth;
232 if (cid == m_Depth - 1U || nextTail == m_SubmissionHead) {
233 stopLocked();
234 return Result::TransportError;
235 }
236 auto& slot = m_Slots[cid];
237 [[maybe_unused]] const size_t drained = slot.completion.drainAvailable();
238 slot.active = true;
239 if (++m_Outstanding > m_MaximumOutstanding)
240 m_MaximumOutstanding = m_Outstanding;
241 slot.done = false;
242 slot.status = 0;
243 if (bytes) {
244 if (writing && !directWrite) {
245 MemoryCopy(slot.data.virtualAddress(), buffer, bytes);
246 }
247 command.prp1 = directWrite ? directWritePhysical : direct ? directPage : slot.firstPage;
248 command.prp2 = directWrite || bytes <= Nvme::PageSize ? 0
249 : bytes <= 2 * Nvme::PageSize
250 ? static_cast<uint64_t*>(slot.prps.virtualAddress())[0]
251 : slot.prpsDma.address();
252 }
253 command.opcode = (command.opcode & 0xffffU) | (cid << 16);
254 auto* submission = static_cast<Nvme::Command*>(m_Submission.virtualAddress());
255 submission[m_Tail] = command;
256 m_Tail = nextTail;
257 FENCE();
258 m_Registers->write32(m_Tail, Nvme::Doorbells + 2U * m_Id * m_Stride);
259 (void)m_Registers->read32(Nvme::Status);
260 }
261 const auto deadline = Time::getTicks() + timeoutSeconds * Time::Multiplier::Second;
262 bool interruptGrace = interruptProbe;
263 for (;;) {
264 bool poll = !interrupts;
265 // Ordinary commands retain their existing poll-recovery latency. The
266 // readiness probe first leaves the CQ entry available to the IRQ worker.
267 if (interrupts)
268 poll = !m_Slots[cid].completion.acquireForCompletion(1, interruptGrace ? 1 : 0,
269 interruptGrace ? 0 : 10000);
270 interruptGrace = false;
271 {
272 LockGuard<Mutex> guard(m_Lock);
273 if (poll)
274 observe(false, interrupts);
275 auto& slot = m_Slots[cid];
276 if (!m_Online || (m_Registers->read32(Nvme::Status) & 2U)) {
277 stopLocked();
278 slot.active = false;
279 return Result::TransportError;
280 }
281 if (slot.done) {
282 const bool success = !slot.status;
283 if (success && bytes && !writing) {
284 FENCE();
285 if (!direct) {
286 MemoryCopy(buffer, slot.data.virtualAddress(), bytes);
287 }
288 }
289 if (result)
290 *result = slot.result;
291 if (!success)
292 WARNING("NVMe: command " << Hex << (command.opcode & 255U) << " namespace "
293 << command.nsid << " status " << slot.status);
294 slot.active = false;
295 m_Available.release();
296 return success ? Result::Success : Result::CommandError;
297 }
298 if (Time::getTicks() >= deadline) {
299 ERROR("NVMe: command timeout on queue " << m_Id << ", CID " << cid);
300 stopLocked();
301 slot.active = false;
302 return Result::TransportError;
303 }
304 }
305 if (!interrupts) {
306#if ARM64
307 // Queue setup polls before INTx is registered. A timed sleep can park
308 // the only runnable thread before a completion is consumed.
310#else
311 Time::delay(Time::Multiplier::Millisecond);
312#endif
313 }
314 }
315}
316size_t NvmeQueue::interruptCompletions() const {
317 LockGuard<Mutex> guard(m_Lock);
318 return m_InterruptCompletions;
319}
320size_t NvmeQueue::maximumOutstanding() const {
321 LockGuard<Mutex> guard(m_Lock);
322 return m_MaximumOutstanding;
323}
Abstrace base class for hardware I/O capabilities.
Definition IoBase.h:32
virtual void write32(uint32_t value, size_t offset=0)=0
virtual uint32_t read32(size_t offset=0)=0
void * virtualAddress() const
physical_uintptr_t physicalAddress() const
void releaseDma()
Definition NvmeQueue.cc:118
static PhysicalMemoryManager & instance()
static void pause()
void release(size_t n=1)
Definition Semaphore.cc:549
MUST_USE_RESULT size_t drainAvailable()
Definition Semaphore.cc:531
MUST_USE_RESULT bool acquireForCompletion(size_t n=1, size_t timeoutSecs=0, size_t timeoutUsecs=0)
Definition Semaphore.cc:372
static constexpr size_t getPageSize() noexcept
Definition TargetInfo.h:40
virtual bool getMapping(void *virtualAddress, physical_uintptr_t &physicalAddress, size_t &flags)=0
static EXPORTED_PUBLIC VirtualAddressSpace & getKernelAddressSpace()
@ Hex
Definition Log.h:124