@wenathlan/saddle 1.8.18 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (802) hide show
  1. package/Dockerfile +1339 -0
  2. package/README.md +33 -12
  3. package/alternatives.ts +1218 -0
  4. package/biome.json +153 -0
  5. package/boards.json +1517 -0
  6. package/compute.ts +3144 -0
  7. package/cores.json +1801 -0
  8. package/dist/acquisition.d.ts +359 -0
  9. package/dist/acquisition.d.ts.map +1 -0
  10. package/dist/acquisition.js +629 -0
  11. package/dist/acquisition.js.map +1 -0
  12. package/dist/alternatives.d.ts +509 -0
  13. package/dist/alternatives.d.ts.map +1 -0
  14. package/dist/alternatives.js +951 -0
  15. package/dist/alternatives.js.map +1 -0
  16. package/dist/automation.d.ts +426 -0
  17. package/dist/automation.d.ts.map +1 -0
  18. package/dist/automation.js +561 -0
  19. package/dist/automation.js.map +1 -0
  20. package/dist/browser.d.ts +562 -0
  21. package/dist/browser.d.ts.map +1 -0
  22. package/dist/browser.js +1057 -0
  23. package/dist/browser.js.map +1 -0
  24. package/dist/capacitor.config.d.ts +13 -0
  25. package/dist/capacitor.config.d.ts.map +1 -0
  26. package/dist/capacitor.config.js +26 -0
  27. package/dist/capacitor.config.js.map +1 -0
  28. package/dist/cli.d.ts +17 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +111 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/communication.d.ts +192 -0
  33. package/dist/communication.d.ts.map +1 -0
  34. package/dist/communication.js +418 -0
  35. package/dist/communication.js.map +1 -0
  36. package/dist/compute.d.ts +1161 -0
  37. package/dist/compute.d.ts.map +1 -0
  38. package/dist/compute.js +2273 -0
  39. package/dist/compute.js.map +1 -0
  40. package/dist/distribution.d.ts +529 -0
  41. package/dist/distribution.d.ts.map +1 -0
  42. package/dist/distribution.js +811 -0
  43. package/dist/distribution.js.map +1 -0
  44. package/dist/execution.d.ts +530 -0
  45. package/dist/execution.d.ts.map +1 -0
  46. package/dist/execution.js +761 -0
  47. package/dist/execution.js.map +1 -0
  48. package/dist/format.d.ts +19 -0
  49. package/dist/format.d.ts.map +1 -0
  50. package/dist/format.js +60 -0
  51. package/dist/format.js.map +1 -0
  52. package/dist/foundation.d.ts +176 -0
  53. package/dist/foundation.d.ts.map +1 -0
  54. package/dist/foundation.js +309 -0
  55. package/dist/foundation.js.map +1 -0
  56. package/dist/index.d.ts +733 -123
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +1018 -123
  59. package/dist/index.js.map +1 -1
  60. package/dist/integration.d.ts +236 -0
  61. package/dist/integration.d.ts.map +1 -0
  62. package/dist/integration.js +236 -0
  63. package/dist/integration.js.map +1 -0
  64. package/dist/intelligence.d.ts +94 -0
  65. package/dist/intelligence.d.ts.map +1 -0
  66. package/dist/intelligence.js +145 -0
  67. package/dist/intelligence.js.map +1 -0
  68. package/dist/{isolation/contracts.d.ts → isolation.d.ts} +12 -1
  69. package/dist/isolation.d.ts.map +1 -0
  70. package/dist/{isolation/contracts.js → isolation.js} +13 -1
  71. package/dist/isolation.js.map +1 -0
  72. package/dist/media.d.ts +344 -0
  73. package/dist/media.d.ts.map +1 -0
  74. package/dist/media.js +986 -0
  75. package/dist/media.js.map +1 -0
  76. package/dist/modes.d.ts +359 -0
  77. package/dist/modes.d.ts.map +1 -0
  78. package/dist/modes.js +134 -0
  79. package/dist/modes.js.map +1 -0
  80. package/dist/{library/public.d.ts → operations.d.ts} +15 -1
  81. package/dist/operations.d.ts.map +1 -0
  82. package/dist/{library/public.js → operations.js} +18 -7
  83. package/dist/operations.js.map +1 -0
  84. package/dist/orchestrator.d.ts +1339 -0
  85. package/dist/orchestrator.d.ts.map +1 -0
  86. package/dist/orchestrator.js +4308 -0
  87. package/dist/orchestrator.js.map +1 -0
  88. package/dist/performance.d.ts +416 -0
  89. package/dist/performance.d.ts.map +1 -0
  90. package/dist/performance.js +827 -0
  91. package/dist/performance.js.map +1 -0
  92. package/dist/quantum.d.ts +810 -0
  93. package/dist/quantum.d.ts.map +1 -0
  94. package/dist/quantum.js +2073 -0
  95. package/dist/quantum.js.map +1 -0
  96. package/dist/render.d.ts +381 -0
  97. package/dist/render.d.ts.map +1 -0
  98. package/dist/render.js +1073 -0
  99. package/dist/render.js.map +1 -0
  100. package/dist/scheduler.d.ts +596 -0
  101. package/dist/scheduler.d.ts.map +1 -0
  102. package/dist/scheduler.js +1259 -0
  103. package/dist/scheduler.js.map +1 -0
  104. package/dist/security.d.ts +501 -0
  105. package/dist/security.d.ts.map +1 -0
  106. package/dist/security.js +1170 -0
  107. package/dist/security.js.map +1 -0
  108. package/dist/server.d.ts +21 -0
  109. package/dist/server.d.ts.map +1 -0
  110. package/dist/{server/node.js → server.js} +16 -2
  111. package/dist/server.js.map +1 -0
  112. package/dist/tiers.d.ts +1002 -0
  113. package/dist/tiers.d.ts.map +1 -0
  114. package/dist/tiers.js +2165 -0
  115. package/dist/tiers.js.map +1 -0
  116. package/dist/virtual.d.ts +641 -0
  117. package/dist/virtual.d.ts.map +1 -0
  118. package/dist/virtual.js +1285 -0
  119. package/dist/virtual.js.map +1 -0
  120. package/dist/virtualcpu.d.ts +316 -0
  121. package/dist/virtualcpu.d.ts.map +1 -0
  122. package/dist/virtualcpu.js +1180 -0
  123. package/dist/virtualcpu.js.map +1 -0
  124. package/dist/virtualgpu.d.ts +345 -0
  125. package/dist/virtualgpu.d.ts.map +1 -0
  126. package/dist/virtualgpu.js +1000 -0
  127. package/dist/virtualgpu.js.map +1 -0
  128. package/dist/virtualization.d.ts +607 -0
  129. package/dist/virtualization.d.ts.map +1 -0
  130. package/dist/virtualization.js +1187 -0
  131. package/dist/virtualization.js.map +1 -0
  132. package/dist/virtualmemory.d.ts +818 -0
  133. package/dist/virtualmemory.d.ts.map +1 -0
  134. package/dist/virtualmemory.js +1796 -0
  135. package/dist/virtualmemory.js.map +1 -0
  136. package/dist/webscrape.d.ts +720 -0
  137. package/dist/webscrape.d.ts.map +1 -0
  138. package/dist/webscrape.js +2697 -0
  139. package/dist/webscrape.js.map +1 -0
  140. package/docker.config +769 -0
  141. package/docs/CONVERSA.txt +1523 -0
  142. package/docs/alternatives.md +499 -0
  143. package/docs/architecture-1.8.19.md +76 -0
  144. package/docs/architecture-1.8.19.mmd +33 -0
  145. package/docs/architecture-1.8.19.png +0 -0
  146. package/docs/architecture.md +355 -0
  147. package/docs/brancharchive-2026-08-18.md +18 -0
  148. package/docs/browser.md +7 -0
  149. package/docs/consolidation.md +70 -0
  150. package/docs/e2ugh-engine.md +528 -0
  151. package/{dist/examples/localjob.js → docs/example-localjob.ts} +3 -3
  152. package/{dist/examples/publicapi.js → docs/example-publicapi.ts} +2 -2
  153. package/docs/hardware.md +342 -0
  154. package/docs/optimization.md +126 -0
  155. package/docs/performance.md +499 -0
  156. package/docs/planning.1.8.18.md +94 -0
  157. package/docs/planning.1.8.19.md +49 -0
  158. package/docs/releasenotes-1.8.19.md +35 -0
  159. package/docs/research-1.8.19-virtual-browser.md +27 -0
  160. package/docs/security.md +454 -0
  161. package/docs/todo-1.8.18.md +12 -0
  162. package/docs/todo-1.8.19.md +241 -0
  163. package/docs/viability.md +777 -0
  164. package/docs/virtualization.md +335 -0
  165. package/docs/web-duplicatecleanup.md +9 -0
  166. package/docs/web-ideas.md +106 -0
  167. package/docs/workflowimprovements-2026-08-19.md +70 -0
  168. package/gpumonitor.cpp +1543 -0
  169. package/gpus.json +2408 -0
  170. package/index.ts +1478 -0
  171. package/media.ts +1337 -0
  172. package/mttg.config +1549 -0
  173. package/orchestrator.ts +5462 -0
  174. package/package.json +304 -56
  175. package/passage.config +1331 -0
  176. package/performance.ts +1049 -0
  177. package/processors.json +2217 -0
  178. package/qemu.config +1062 -0
  179. package/qemubridge.py +1340 -0
  180. package/quantum.ts +2393 -0
  181. package/render.ts +1325 -0
  182. package/scheduler.ts +1600 -0
  183. package/security.ts +1449 -0
  184. package/tiers.ts +2871 -0
  185. package/tsconfig.json +29 -0
  186. package/virtualcpu.ts +1273 -0
  187. package/virtualgpu.ts +1181 -0
  188. package/virtualhardware.c +1011 -0
  189. package/virtualhardware.json +739 -0
  190. package/virtualization.ts +1628 -0
  191. package/virtualizationcore.cpp +4635 -0
  192. package/virtualmemory.ts +2303 -0
  193. package/vm.config.json +1910 -0
  194. package/{extension → web/extension}/manifest.json +1 -1
  195. package/web/readme.md +382 -0
  196. package/web/tsconfig.json +25 -0
  197. package/dist/adapters/forge.d.ts +0 -17
  198. package/dist/adapters/forge.d.ts.map +0 -1
  199. package/dist/adapters/forge.js +0 -19
  200. package/dist/adapters/forge.js.map +0 -1
  201. package/dist/adapters/forgejo.d.ts +0 -49
  202. package/dist/adapters/forgejo.d.ts.map +0 -1
  203. package/dist/adapters/forgejo.js +0 -8
  204. package/dist/adapters/forgejo.js.map +0 -1
  205. package/dist/adapters/github.d.ts +0 -12
  206. package/dist/adapters/github.d.ts.map +0 -1
  207. package/dist/adapters/github.js +0 -20
  208. package/dist/adapters/github.js.map +0 -1
  209. package/dist/adapters/gitlab.d.ts +0 -17
  210. package/dist/adapters/gitlab.d.ts.map +0 -1
  211. package/dist/adapters/gitlab.js +0 -10
  212. package/dist/adapters/gitlab.js.map +0 -1
  213. package/dist/adapters/huggingface.d.ts +0 -17
  214. package/dist/adapters/huggingface.d.ts.map +0 -1
  215. package/dist/adapters/huggingface.js +0 -6
  216. package/dist/adapters/huggingface.js.map +0 -1
  217. package/dist/adapters/socket.d.ts +0 -11
  218. package/dist/adapters/socket.d.ts.map +0 -1
  219. package/dist/adapters/socket.js +0 -17
  220. package/dist/adapters/socket.js.map +0 -1
  221. package/dist/adapters/transport.d.ts +0 -7
  222. package/dist/adapters/transport.d.ts.map +0 -1
  223. package/dist/adapters/transport.js +0 -36
  224. package/dist/adapters/transport.js.map +0 -1
  225. package/dist/ai/chunk.d.ts +0 -2
  226. package/dist/ai/chunk.d.ts.map +0 -1
  227. package/dist/ai/chunk.js +0 -36
  228. package/dist/ai/chunk.js.map +0 -1
  229. package/dist/ai/llmstxt.d.ts +0 -6
  230. package/dist/ai/llmstxt.d.ts.map +0 -1
  231. package/dist/ai/llmstxt.js +0 -12
  232. package/dist/ai/llmstxt.js.map +0 -1
  233. package/dist/ai/provenance.d.ts +0 -22
  234. package/dist/ai/provenance.d.ts.map +0 -1
  235. package/dist/ai/provenance.js +0 -25
  236. package/dist/ai/provenance.js.map +0 -1
  237. package/dist/ai/rag.d.ts +0 -26
  238. package/dist/ai/rag.d.ts.map +0 -1
  239. package/dist/ai/rag.js +0 -22
  240. package/dist/ai/rag.js.map +0 -1
  241. package/dist/ai/tokens.d.ts +0 -15
  242. package/dist/ai/tokens.d.ts.map +0 -1
  243. package/dist/ai/tokens.js +0 -9
  244. package/dist/ai/tokens.js.map +0 -1
  245. package/dist/api/auth.d.ts +0 -16
  246. package/dist/api/auth.d.ts.map +0 -1
  247. package/dist/api/auth.js +0 -19
  248. package/dist/api/auth.js.map +0 -1
  249. package/dist/api/contracts.d.ts +0 -28
  250. package/dist/api/contracts.d.ts.map +0 -1
  251. package/dist/api/contracts.js +0 -14
  252. package/dist/api/contracts.js.map +0 -1
  253. package/dist/api/control.d.ts +0 -10
  254. package/dist/api/control.d.ts.map +0 -1
  255. package/dist/api/control.js +0 -35
  256. package/dist/api/control.js.map +0 -1
  257. package/dist/api/http.d.ts +0 -7
  258. package/dist/api/http.d.ts.map +0 -1
  259. package/dist/api/http.js +0 -12
  260. package/dist/api/http.js.map +0 -1
  261. package/dist/api/rate.d.ts +0 -28
  262. package/dist/api/rate.d.ts.map +0 -1
  263. package/dist/api/rate.js +0 -36
  264. package/dist/api/rate.js.map +0 -1
  265. package/dist/api/security.d.ts +0 -12
  266. package/dist/api/security.d.ts.map +0 -1
  267. package/dist/api/security.js +0 -56
  268. package/dist/api/security.js.map +0 -1
  269. package/dist/api/service.d.ts +0 -5
  270. package/dist/api/service.d.ts.map +0 -1
  271. package/dist/api/service.js +0 -70
  272. package/dist/api/service.js.map +0 -1
  273. package/dist/apps/registry.d.ts +0 -33
  274. package/dist/apps/registry.d.ts.map +0 -1
  275. package/dist/apps/registry.js +0 -24
  276. package/dist/apps/registry.js.map +0 -1
  277. package/dist/binary/archive.d.ts +0 -27
  278. package/dist/binary/archive.d.ts.map +0 -1
  279. package/dist/binary/archive.js +0 -46
  280. package/dist/binary/archive.js.map +0 -1
  281. package/dist/binary/build.d.ts +0 -29
  282. package/dist/binary/build.d.ts.map +0 -1
  283. package/dist/binary/build.js +0 -17
  284. package/dist/binary/build.js.map +0 -1
  285. package/dist/binary/transform.d.ts +0 -70
  286. package/dist/binary/transform.d.ts.map +0 -1
  287. package/dist/binary/transform.js +0 -105
  288. package/dist/binary/transform.js.map +0 -1
  289. package/dist/bot/adapter.d.ts +0 -5
  290. package/dist/bot/adapter.d.ts.map +0 -1
  291. package/dist/bot/adapter.js +0 -11
  292. package/dist/bot/adapter.js.map +0 -1
  293. package/dist/bot/bot.d.ts +0 -28
  294. package/dist/bot/bot.d.ts.map +0 -1
  295. package/dist/bot/bot.js +0 -52
  296. package/dist/bot/bot.js.map +0 -1
  297. package/dist/bot/commands.d.ts +0 -8
  298. package/dist/bot/commands.d.ts.map +0 -1
  299. package/dist/bot/commands.js +0 -19
  300. package/dist/bot/commands.js.map +0 -1
  301. package/dist/bot/permissions.d.ts +0 -13
  302. package/dist/bot/permissions.d.ts.map +0 -1
  303. package/dist/bot/permissions.js +0 -16
  304. package/dist/bot/permissions.js.map +0 -1
  305. package/dist/browser/actions.d.ts +0 -33
  306. package/dist/browser/actions.d.ts.map +0 -1
  307. package/dist/browser/actions.js +0 -44
  308. package/dist/browser/actions.js.map +0 -1
  309. package/dist/browser/agent.d.ts +0 -21
  310. package/dist/browser/agent.d.ts.map +0 -1
  311. package/dist/browser/agent.js +0 -13
  312. package/dist/browser/agent.js.map +0 -1
  313. package/dist/browser/context.d.ts +0 -43
  314. package/dist/browser/context.d.ts.map +0 -1
  315. package/dist/browser/context.js +0 -54
  316. package/dist/browser/context.js.map +0 -1
  317. package/dist/browser/fingerprint.d.ts +0 -4
  318. package/dist/browser/fingerprint.d.ts.map +0 -1
  319. package/dist/browser/fingerprint.js +0 -12
  320. package/dist/browser/fingerprint.js.map +0 -1
  321. package/dist/browser/index.d.ts +0 -11
  322. package/dist/browser/index.d.ts.map +0 -1
  323. package/dist/browser/index.js +0 -11
  324. package/dist/browser/index.js.map +0 -1
  325. package/dist/browser/playwright.d.ts +0 -11
  326. package/dist/browser/playwright.d.ts.map +0 -1
  327. package/dist/browser/playwright.js +0 -24
  328. package/dist/browser/playwright.js.map +0 -1
  329. package/dist/browser/recorder.d.ts +0 -21
  330. package/dist/browser/recorder.d.ts.map +0 -1
  331. package/dist/browser/recorder.js +0 -58
  332. package/dist/browser/recorder.js.map +0 -1
  333. package/dist/browser/session.d.ts +0 -14
  334. package/dist/browser/session.d.ts.map +0 -1
  335. package/dist/browser/session.js +0 -21
  336. package/dist/browser/session.js.map +0 -1
  337. package/dist/browser/snapshot.d.ts +0 -69
  338. package/dist/browser/snapshot.d.ts.map +0 -1
  339. package/dist/browser/snapshot.js +0 -133
  340. package/dist/browser/snapshot.js.map +0 -1
  341. package/dist/captcha/contract.d.ts +0 -35
  342. package/dist/captcha/contract.d.ts.map +0 -1
  343. package/dist/captcha/contract.js +0 -17
  344. package/dist/captcha/contract.js.map +0 -1
  345. package/dist/captcha/evidence.d.ts +0 -10
  346. package/dist/captcha/evidence.d.ts.map +0 -1
  347. package/dist/captcha/evidence.js +0 -9
  348. package/dist/captcha/evidence.js.map +0 -1
  349. package/dist/captcha/guard.d.ts +0 -23
  350. package/dist/captcha/guard.d.ts.map +0 -1
  351. package/dist/captcha/guard.js +0 -12
  352. package/dist/captcha/guard.js.map +0 -1
  353. package/dist/cli/main.d.ts +0 -4
  354. package/dist/cli/main.d.ts.map +0 -1
  355. package/dist/cli/main.js +0 -46
  356. package/dist/cli/main.js.map +0 -1
  357. package/dist/core/errors.d.ts +0 -84
  358. package/dist/core/errors.d.ts.map +0 -1
  359. package/dist/core/errors.js +0 -54
  360. package/dist/core/errors.js.map +0 -1
  361. package/dist/core/events.d.ts +0 -9
  362. package/dist/core/events.d.ts.map +0 -1
  363. package/dist/core/events.js +0 -21
  364. package/dist/core/events.js.map +0 -1
  365. package/dist/core/hash.d.ts +0 -10
  366. package/dist/core/hash.d.ts.map +0 -1
  367. package/dist/core/hash.js +0 -82
  368. package/dist/core/hash.js.map +0 -1
  369. package/dist/core/ids.d.ts +0 -10
  370. package/dist/core/ids.d.ts.map +0 -1
  371. package/dist/core/ids.js +0 -15
  372. package/dist/core/ids.js.map +0 -1
  373. package/dist/delivery/manifest.d.ts +0 -35
  374. package/dist/delivery/manifest.d.ts.map +0 -1
  375. package/dist/delivery/manifest.js +0 -68
  376. package/dist/delivery/manifest.js.map +0 -1
  377. package/dist/deploy/index.d.ts +0 -6
  378. package/dist/deploy/index.d.ts.map +0 -1
  379. package/dist/deploy/index.js +0 -6
  380. package/dist/deploy/index.js.map +0 -1
  381. package/dist/dispatch/resumable.d.ts +0 -102
  382. package/dist/dispatch/resumable.d.ts.map +0 -1
  383. package/dist/dispatch/resumable.js +0 -64
  384. package/dist/dispatch/resumable.js.map +0 -1
  385. package/dist/dispatch/workflow.d.ts +0 -6
  386. package/dist/dispatch/workflow.d.ts.map +0 -1
  387. package/dist/dispatch/workflow.js +0 -37
  388. package/dist/dispatch/workflow.js.map +0 -1
  389. package/dist/domain/artifacts.d.ts +0 -12
  390. package/dist/domain/artifacts.d.ts.map +0 -1
  391. package/dist/domain/artifacts.js +0 -14
  392. package/dist/domain/artifacts.js.map +0 -1
  393. package/dist/domain/jobs.d.ts +0 -12
  394. package/dist/domain/jobs.d.ts.map +0 -1
  395. package/dist/domain/jobs.js +0 -20
  396. package/dist/domain/jobs.js.map +0 -1
  397. package/dist/domain/providers.d.ts +0 -10
  398. package/dist/domain/providers.d.ts.map +0 -1
  399. package/dist/domain/providers.js +0 -8
  400. package/dist/domain/providers.js.map +0 -1
  401. package/dist/domain/runtime.d.ts +0 -14
  402. package/dist/domain/runtime.d.ts.map +0 -1
  403. package/dist/domain/runtime.js +0 -10
  404. package/dist/domain/runtime.js.map +0 -1
  405. package/dist/domain/sessions.d.ts +0 -12
  406. package/dist/domain/sessions.d.ts.map +0 -1
  407. package/dist/domain/sessions.js +0 -57
  408. package/dist/domain/sessions.js.map +0 -1
  409. package/dist/examples/localjob.d.ts +0 -2
  410. package/dist/examples/localjob.d.ts.map +0 -1
  411. package/dist/examples/localjob.js.map +0 -1
  412. package/dist/examples/publicapi.d.ts +0 -2
  413. package/dist/examples/publicapi.d.ts.map +0 -1
  414. package/dist/examples/publicapi.js.map +0 -1
  415. package/dist/extension/build.d.ts +0 -9
  416. package/dist/extension/build.d.ts.map +0 -1
  417. package/dist/extension/build.js +0 -85
  418. package/dist/extension/build.js.map +0 -1
  419. package/dist/extension/content.d.ts +0 -6
  420. package/dist/extension/content.d.ts.map +0 -1
  421. package/dist/extension/content.js +0 -135
  422. package/dist/extension/content.js.map +0 -1
  423. package/dist/extension/index.d.ts +0 -7
  424. package/dist/extension/index.d.ts.map +0 -1
  425. package/dist/extension/index.js +0 -7
  426. package/dist/extension/index.js.map +0 -1
  427. package/dist/extension/pagebridge.d.ts +0 -6
  428. package/dist/extension/pagebridge.d.ts.map +0 -1
  429. package/dist/extension/pagebridge.js +0 -42
  430. package/dist/extension/pagebridge.js.map +0 -1
  431. package/dist/extension/permissions.d.ts +0 -26
  432. package/dist/extension/permissions.d.ts.map +0 -1
  433. package/dist/extension/permissions.js +0 -28
  434. package/dist/extension/permissions.js.map +0 -1
  435. package/dist/extension/popup.d.ts +0 -5
  436. package/dist/extension/popup.d.ts.map +0 -1
  437. package/dist/extension/popup.js +0 -26
  438. package/dist/extension/popup.js.map +0 -1
  439. package/dist/extension/protocol.d.ts +0 -58
  440. package/dist/extension/protocol.d.ts.map +0 -1
  441. package/dist/extension/protocol.js +0 -101
  442. package/dist/extension/protocol.js.map +0 -1
  443. package/dist/extension/serviceworker.d.ts +0 -25
  444. package/dist/extension/serviceworker.d.ts.map +0 -1
  445. package/dist/extension/serviceworker.js +0 -102
  446. package/dist/extension/serviceworker.js.map +0 -1
  447. package/dist/extension/worker.d.ts +0 -28
  448. package/dist/extension/worker.d.ts.map +0 -1
  449. package/dist/extension/worker.js +0 -22
  450. package/dist/extension/worker.js.map +0 -1
  451. package/dist/format/check.d.ts +0 -8
  452. package/dist/format/check.d.ts.map +0 -1
  453. package/dist/format/check.js +0 -38
  454. package/dist/format/check.js.map +0 -1
  455. package/dist/isolation/contracts.d.ts.map +0 -1
  456. package/dist/isolation/contracts.js.map +0 -1
  457. package/dist/library/public.d.ts.map +0 -1
  458. package/dist/library/public.js.map +0 -1
  459. package/dist/mcp/browser.d.ts +0 -9
  460. package/dist/mcp/browser.d.ts.map +0 -1
  461. package/dist/mcp/browser.js +0 -13
  462. package/dist/mcp/browser.js.map +0 -1
  463. package/dist/mcp/server.d.ts +0 -35
  464. package/dist/mcp/server.d.ts.map +0 -1
  465. package/dist/mcp/server.js +0 -40
  466. package/dist/mcp/server.js.map +0 -1
  467. package/dist/mcp/transport.d.ts +0 -7
  468. package/dist/mcp/transport.d.ts.map +0 -1
  469. package/dist/mcp/transport.js +0 -17
  470. package/dist/mcp/transport.js.map +0 -1
  471. package/dist/memory/bridge.d.ts +0 -14
  472. package/dist/memory/bridge.d.ts.map +0 -1
  473. package/dist/memory/bridge.js +0 -16
  474. package/dist/memory/bridge.js.map +0 -1
  475. package/dist/memory/engine.d.ts +0 -38
  476. package/dist/memory/engine.d.ts.map +0 -1
  477. package/dist/memory/engine.js +0 -109
  478. package/dist/memory/engine.js.map +0 -1
  479. package/dist/memory/modes.d.ts +0 -43
  480. package/dist/memory/modes.d.ts.map +0 -1
  481. package/dist/memory/modes.js +0 -66
  482. package/dist/memory/modes.js.map +0 -1
  483. package/dist/memory/objects.d.ts +0 -21
  484. package/dist/memory/objects.d.ts.map +0 -1
  485. package/dist/memory/objects.js +0 -20
  486. package/dist/memory/objects.js.map +0 -1
  487. package/dist/memory/planner.d.ts +0 -66
  488. package/dist/memory/planner.d.ts.map +0 -1
  489. package/dist/memory/planner.js +0 -108
  490. package/dist/memory/planner.js.map +0 -1
  491. package/dist/memory/targets.d.ts +0 -78
  492. package/dist/memory/targets.d.ts.map +0 -1
  493. package/dist/memory/targets.js +0 -29
  494. package/dist/memory/targets.js.map +0 -1
  495. package/dist/memory/transforms.d.ts +0 -19
  496. package/dist/memory/transforms.d.ts.map +0 -1
  497. package/dist/memory/transforms.js +0 -14
  498. package/dist/memory/transforms.js.map +0 -1
  499. package/dist/modes/matrix.d.ts +0 -23
  500. package/dist/modes/matrix.d.ts.map +0 -1
  501. package/dist/modes/matrix.js +0 -18
  502. package/dist/modes/matrix.js.map +0 -1
  503. package/dist/modes/modes.d.ts +0 -33
  504. package/dist/modes/modes.d.ts.map +0 -1
  505. package/dist/modes/modes.js +0 -15
  506. package/dist/modes/modes.js.map +0 -1
  507. package/dist/modes/resolve.d.ts +0 -96
  508. package/dist/modes/resolve.d.ts.map +0 -1
  509. package/dist/modes/resolve.js +0 -48
  510. package/dist/modes/resolve.js.map +0 -1
  511. package/dist/observability/metrics.d.ts +0 -14
  512. package/dist/observability/metrics.d.ts.map +0 -1
  513. package/dist/observability/metrics.js +0 -18
  514. package/dist/observability/metrics.js.map +0 -1
  515. package/dist/packager/manifest.d.ts +0 -66
  516. package/dist/packager/manifest.d.ts.map +0 -1
  517. package/dist/packager/manifest.js +0 -94
  518. package/dist/packager/manifest.js.map +0 -1
  519. package/dist/packager/publish.d.ts +0 -51
  520. package/dist/packager/publish.d.ts.map +0 -1
  521. package/dist/packager/publish.js +0 -18
  522. package/dist/packager/publish.js.map +0 -1
  523. package/dist/packager/targetcli.d.ts +0 -7
  524. package/dist/packager/targetcli.d.ts.map +0 -1
  525. package/dist/packager/targetcli.js +0 -22
  526. package/dist/packager/targetcli.js.map +0 -1
  527. package/dist/persistence/adapter.d.ts +0 -5
  528. package/dist/persistence/adapter.d.ts.map +0 -1
  529. package/dist/persistence/adapter.js +0 -11
  530. package/dist/persistence/adapter.js.map +0 -1
  531. package/dist/persistence/drizzle.d.ts +0 -2
  532. package/dist/persistence/drizzle.d.ts.map +0 -1
  533. package/dist/persistence/drizzle.js +0 -12
  534. package/dist/persistence/drizzle.js.map +0 -1
  535. package/dist/persistence/memory.d.ts +0 -2
  536. package/dist/persistence/memory.d.ts.map +0 -1
  537. package/dist/persistence/memory.js +0 -27
  538. package/dist/persistence/memory.js.map +0 -1
  539. package/dist/persistence/migrations.d.ts +0 -19
  540. package/dist/persistence/migrations.d.ts.map +0 -1
  541. package/dist/persistence/migrations.js +0 -13
  542. package/dist/persistence/migrations.js.map +0 -1
  543. package/dist/persistence/prisma.d.ts +0 -2
  544. package/dist/persistence/prisma.d.ts.map +0 -1
  545. package/dist/persistence/prisma.js +0 -25
  546. package/dist/persistence/prisma.js.map +0 -1
  547. package/dist/persistence/schema.d.ts +0 -63
  548. package/dist/persistence/schema.d.ts.map +0 -1
  549. package/dist/persistence/schema.js +0 -28
  550. package/dist/persistence/schema.js.map +0 -1
  551. package/dist/persistence/sql.d.ts +0 -3
  552. package/dist/persistence/sql.d.ts.map +0 -1
  553. package/dist/persistence/sql.js +0 -37
  554. package/dist/persistence/sql.js.map +0 -1
  555. package/dist/protocol/blocks.d.ts +0 -10
  556. package/dist/protocol/blocks.d.ts.map +0 -1
  557. package/dist/protocol/blocks.js +0 -26
  558. package/dist/protocol/blocks.js.map +0 -1
  559. package/dist/protocol/json.d.ts +0 -6
  560. package/dist/protocol/json.d.ts.map +0 -1
  561. package/dist/protocol/json.js +0 -6
  562. package/dist/protocol/json.js.map +0 -1
  563. package/dist/protocol/ndjson.d.ts +0 -3
  564. package/dist/protocol/ndjson.d.ts.map +0 -1
  565. package/dist/protocol/ndjson.js +0 -20
  566. package/dist/protocol/ndjson.js.map +0 -1
  567. package/dist/protocol/sse.d.ts +0 -7
  568. package/dist/protocol/sse.d.ts.map +0 -1
  569. package/dist/protocol/sse.js +0 -25
  570. package/dist/protocol/sse.js.map +0 -1
  571. package/dist/proxy/pool.d.ts +0 -9
  572. package/dist/proxy/pool.d.ts.map +0 -1
  573. package/dist/proxy/pool.js +0 -27
  574. package/dist/proxy/pool.js.map +0 -1
  575. package/dist/queue/idempotency.d.ts +0 -10
  576. package/dist/queue/idempotency.d.ts.map +0 -1
  577. package/dist/queue/idempotency.js +0 -13
  578. package/dist/queue/idempotency.js.map +0 -1
  579. package/dist/queue/persistent.d.ts +0 -14
  580. package/dist/queue/persistent.d.ts.map +0 -1
  581. package/dist/queue/persistent.js +0 -66
  582. package/dist/queue/persistent.js.map +0 -1
  583. package/dist/queue/queue.d.ts +0 -7
  584. package/dist/queue/queue.d.ts.map +0 -1
  585. package/dist/queue/queue.js +0 -55
  586. package/dist/queue/queue.js.map +0 -1
  587. package/dist/queue/saga.d.ts +0 -5
  588. package/dist/queue/saga.d.ts.map +0 -1
  589. package/dist/queue/saga.js +0 -20
  590. package/dist/queue/saga.js.map +0 -1
  591. package/dist/release/assets.d.ts +0 -144
  592. package/dist/release/assets.d.ts.map +0 -1
  593. package/dist/release/assets.js +0 -157
  594. package/dist/release/assets.js.map +0 -1
  595. package/dist/release/evidence.d.ts +0 -92
  596. package/dist/release/evidence.d.ts.map +0 -1
  597. package/dist/release/evidence.js +0 -148
  598. package/dist/release/evidence.js.map +0 -1
  599. package/dist/release/verify.d.ts +0 -12
  600. package/dist/release/verify.d.ts.map +0 -1
  601. package/dist/release/verify.js +0 -112
  602. package/dist/release/verify.js.map +0 -1
  603. package/dist/runners/chain.d.ts +0 -125
  604. package/dist/runners/chain.d.ts.map +0 -1
  605. package/dist/runners/chain.js +0 -95
  606. package/dist/runners/chain.js.map +0 -1
  607. package/dist/runners/health.d.ts +0 -36
  608. package/dist/runners/health.d.ts.map +0 -1
  609. package/dist/runners/health.js +0 -26
  610. package/dist/runners/health.js.map +0 -1
  611. package/dist/runners/heartbeat.d.ts +0 -23
  612. package/dist/runners/heartbeat.d.ts.map +0 -1
  613. package/dist/runners/heartbeat.js +0 -29
  614. package/dist/runners/heartbeat.js.map +0 -1
  615. package/dist/runners/inprocess.d.ts +0 -17
  616. package/dist/runners/inprocess.d.ts.map +0 -1
  617. package/dist/runners/inprocess.js +0 -20
  618. package/dist/runners/inprocess.js.map +0 -1
  619. package/dist/runners/scheduler.d.ts +0 -5
  620. package/dist/runners/scheduler.d.ts.map +0 -1
  621. package/dist/runners/scheduler.js +0 -19
  622. package/dist/runners/scheduler.js.map +0 -1
  623. package/dist/runtime/abort.d.ts +0 -8
  624. package/dist/runtime/abort.d.ts.map +0 -1
  625. package/dist/runtime/abort.js +0 -13
  626. package/dist/runtime/abort.js.map +0 -1
  627. package/dist/runtime/compatibility.d.ts +0 -37
  628. package/dist/runtime/compatibility.d.ts.map +0 -1
  629. package/dist/runtime/compatibility.js +0 -10
  630. package/dist/runtime/compatibility.js.map +0 -1
  631. package/dist/runtime/detect.d.ts +0 -13
  632. package/dist/runtime/detect.d.ts.map +0 -1
  633. package/dist/runtime/detect.js +0 -18
  634. package/dist/runtime/detect.js.map +0 -1
  635. package/dist/runtime/engine.d.ts +0 -20
  636. package/dist/runtime/engine.d.ts.map +0 -1
  637. package/dist/runtime/engine.js +0 -61
  638. package/dist/runtime/engine.js.map +0 -1
  639. package/dist/runtime/retry.d.ts +0 -19
  640. package/dist/runtime/retry.d.ts.map +0 -1
  641. package/dist/runtime/retry.js +0 -54
  642. package/dist/runtime/retry.js.map +0 -1
  643. package/dist/runtime/worker.d.ts +0 -10
  644. package/dist/runtime/worker.d.ts.map +0 -1
  645. package/dist/runtime/worker.js +0 -25
  646. package/dist/runtime/worker.js.map +0 -1
  647. package/dist/scrape/cache.d.ts +0 -45
  648. package/dist/scrape/cache.d.ts.map +0 -1
  649. package/dist/scrape/cache.js +0 -45
  650. package/dist/scrape/cache.js.map +0 -1
  651. package/dist/scrape/crawl.d.ts +0 -47
  652. package/dist/scrape/crawl.d.ts.map +0 -1
  653. package/dist/scrape/crawl.js +0 -114
  654. package/dist/scrape/crawl.js.map +0 -1
  655. package/dist/scrape/extract.d.ts +0 -63
  656. package/dist/scrape/extract.d.ts.map +0 -1
  657. package/dist/scrape/extract.js +0 -47
  658. package/dist/scrape/extract.js.map +0 -1
  659. package/dist/scrape/normalize.d.ts +0 -52
  660. package/dist/scrape/normalize.d.ts.map +0 -1
  661. package/dist/scrape/normalize.js +0 -102
  662. package/dist/scrape/normalize.js.map +0 -1
  663. package/dist/scrape/robots.d.ts +0 -37
  664. package/dist/scrape/robots.d.ts.map +0 -1
  665. package/dist/scrape/robots.js +0 -71
  666. package/dist/scrape/robots.js.map +0 -1
  667. package/dist/scrape/schema.d.ts +0 -23
  668. package/dist/scrape/schema.d.ts.map +0 -1
  669. package/dist/scrape/schema.js +0 -92
  670. package/dist/scrape/schema.js.map +0 -1
  671. package/dist/scrape/scraper.d.ts +0 -6
  672. package/dist/scrape/scraper.d.ts.map +0 -1
  673. package/dist/scrape/scraper.js +0 -46
  674. package/dist/scrape/scraper.js.map +0 -1
  675. package/dist/scrape/semantic.d.ts +0 -25
  676. package/dist/scrape/semantic.d.ts.map +0 -1
  677. package/dist/scrape/semantic.js +0 -27
  678. package/dist/scrape/semantic.js.map +0 -1
  679. package/dist/server/node.d.ts +0 -7
  680. package/dist/server/node.d.ts.map +0 -1
  681. package/dist/server/node.js.map +0 -1
  682. package/dist/sessions/file.d.ts +0 -25
  683. package/dist/sessions/file.d.ts.map +0 -1
  684. package/dist/sessions/file.js +0 -13
  685. package/dist/sessions/file.js.map +0 -1
  686. package/dist/sessions/replay.d.ts +0 -9
  687. package/dist/sessions/replay.d.ts.map +0 -1
  688. package/dist/sessions/replay.js +0 -70
  689. package/dist/sessions/replay.js.map +0 -1
  690. package/dist/sessions/store.d.ts +0 -25
  691. package/dist/sessions/store.d.ts.map +0 -1
  692. package/dist/sessions/store.js +0 -13
  693. package/dist/sessions/store.js.map +0 -1
  694. package/dist/storage/adapter.d.ts +0 -5
  695. package/dist/storage/adapter.d.ts.map +0 -1
  696. package/dist/storage/adapter.js +0 -11
  697. package/dist/storage/adapter.js.map +0 -1
  698. package/dist/storage/cache.d.ts +0 -23
  699. package/dist/storage/cache.d.ts.map +0 -1
  700. package/dist/storage/cache.js +0 -79
  701. package/dist/storage/cache.js.map +0 -1
  702. package/dist/storage/checksum.d.ts +0 -3
  703. package/dist/storage/checksum.d.ts.map +0 -1
  704. package/dist/storage/checksum.js +0 -19
  705. package/dist/storage/checksum.js.map +0 -1
  706. package/dist/storage/chunked.d.ts +0 -21
  707. package/dist/storage/chunked.d.ts.map +0 -1
  708. package/dist/storage/chunked.js +0 -67
  709. package/dist/storage/chunked.js.map +0 -1
  710. package/dist/storage/content.d.ts +0 -24
  711. package/dist/storage/content.d.ts.map +0 -1
  712. package/dist/storage/content.js +0 -44
  713. package/dist/storage/content.js.map +0 -1
  714. package/dist/storage/filehosting.d.ts +0 -2
  715. package/dist/storage/filehosting.d.ts.map +0 -1
  716. package/dist/storage/filehosting.js +0 -24
  717. package/dist/storage/filehosting.js.map +0 -1
  718. package/dist/storage/githubcontents.d.ts +0 -2
  719. package/dist/storage/githubcontents.d.ts.map +0 -1
  720. package/dist/storage/githubcontents.js +0 -29
  721. package/dist/storage/githubcontents.js.map +0 -1
  722. package/dist/storage/index.d.ts +0 -12
  723. package/dist/storage/index.d.ts.map +0 -1
  724. package/dist/storage/index.js +0 -12
  725. package/dist/storage/index.js.map +0 -1
  726. package/dist/storage/local.d.ts +0 -2
  727. package/dist/storage/local.d.ts.map +0 -1
  728. package/dist/storage/local.js +0 -72
  729. package/dist/storage/local.js.map +0 -1
  730. package/dist/storage/memory.d.ts +0 -6
  731. package/dist/storage/memory.d.ts.map +0 -1
  732. package/dist/storage/memory.js +0 -36
  733. package/dist/storage/memory.js.map +0 -1
  734. package/dist/storage/pool.d.ts +0 -95
  735. package/dist/storage/pool.d.ts.map +0 -1
  736. package/dist/storage/pool.js +0 -202
  737. package/dist/storage/pool.js.map +0 -1
  738. package/dist/storage/s3compatible.d.ts +0 -3
  739. package/dist/storage/s3compatible.d.ts.map +0 -1
  740. package/dist/storage/s3compatible.js +0 -70
  741. package/dist/storage/s3compatible.js.map +0 -1
  742. package/dist/storage/sync.d.ts +0 -47
  743. package/dist/storage/sync.d.ts.map +0 -1
  744. package/dist/storage/sync.js +0 -75
  745. package/dist/storage/sync.js.map +0 -1
  746. package/dist/surfaces/adapters.d.ts +0 -97
  747. package/dist/surfaces/adapters.d.ts.map +0 -1
  748. package/dist/surfaces/adapters.js +0 -50
  749. package/dist/surfaces/adapters.js.map +0 -1
  750. package/dist/surfaces/controls.d.ts +0 -18
  751. package/dist/surfaces/controls.d.ts.map +0 -1
  752. package/dist/surfaces/controls.js +0 -45
  753. package/dist/surfaces/controls.js.map +0 -1
  754. package/dist/surfaces/manifest.d.ts +0 -68
  755. package/dist/surfaces/manifest.d.ts.map +0 -1
  756. package/dist/surfaces/manifest.js +0 -42
  757. package/dist/surfaces/manifest.js.map +0 -1
  758. package/dist/surfaces/n8n.d.ts +0 -25
  759. package/dist/surfaces/n8n.d.ts.map +0 -1
  760. package/dist/surfaces/n8n.js +0 -31
  761. package/dist/surfaces/n8n.js.map +0 -1
  762. package/dist/surfaces/operations.d.ts +0 -32
  763. package/dist/surfaces/operations.d.ts.map +0 -1
  764. package/dist/surfaces/operations.js +0 -53
  765. package/dist/surfaces/operations.js.map +0 -1
  766. package/dist/surfaces/requirements.d.ts +0 -29
  767. package/dist/surfaces/requirements.d.ts.map +0 -1
  768. package/dist/surfaces/requirements.js +0 -50
  769. package/dist/surfaces/requirements.js.map +0 -1
  770. package/dist/surfaces/targets.d.ts +0 -196
  771. package/dist/surfaces/targets.d.ts.map +0 -1
  772. package/dist/surfaces/targets.js +0 -27
  773. package/dist/surfaces/targets.js.map +0 -1
  774. package/dist/webhook/delivery.d.ts +0 -18
  775. package/dist/webhook/delivery.d.ts.map +0 -1
  776. package/dist/webhook/delivery.js +0 -36
  777. package/dist/webhook/delivery.js.map +0 -1
  778. package/dist/webhook/receiver.d.ts +0 -26
  779. package/dist/webhook/receiver.d.ts.map +0 -1
  780. package/dist/webhook/receiver.js +0 -24
  781. package/dist/webhook/receiver.js.map +0 -1
  782. package/dist/webhook/signature.d.ts +0 -3
  783. package/dist/webhook/signature.d.ts.map +0 -1
  784. package/dist/webhook/signature.js +0 -7
  785. package/dist/webhook/signature.js.map +0 -1
  786. package/dist/workflow/manifest.d.ts +0 -22
  787. package/dist/workflow/manifest.d.ts.map +0 -1
  788. package/dist/workflow/manifest.js +0 -21
  789. package/dist/workflow/manifest.js.map +0 -1
  790. package/dist/workflow/registry.d.ts +0 -7
  791. package/dist/workflow/registry.d.ts.map +0 -1
  792. package/dist/workflow/registry.js +0 -18
  793. package/dist/workflow/registry.js.map +0 -1
  794. package/dist/workflow/templates.d.ts +0 -6
  795. package/dist/workflow/templates.d.ts.map +0 -1
  796. package/dist/workflow/templates.js +0 -18
  797. package/dist/workflow/templates.js.map +0 -1
  798. package/dist/workflow/triggers.d.ts +0 -64
  799. package/dist/workflow/triggers.d.ts.map +0 -1
  800. package/dist/workflow/triggers.js +0 -99
  801. package/dist/workflow/triggers.js.map +0 -1
  802. /package/{extension/README.md → docs/extension.md} +0 -0
@@ -0,0 +1,4635 @@
1
+ /*
2
+ * virtualizationcore.cpp — native virtualization core for the virtual
3
+ * hardware engine v2 (vhe), single translation unit.
4
+ *
5
+ * v2 merge of the saddle v5 trio (src_virtualization_core.{hpp,cpp}, real
6
+ * kvm/vfio/qmp ioctls across three namespaces) with the saddle v6 raii
7
+ * reconstruction (virtualizationcore.{hpp,cpp}, FileDescriptor primitives,
8
+ * dirty-log ring, b100/mig profiles, nvlink-c2c, nvenc dual engine). where a
9
+ * class existed in both families the v6 raii version wins and the v5 methods
10
+ * migrate onto it; v5-only contexts are ported unchanged. the v6 header-only
11
+ * stubs collapse into a small contracts section (enums and descriptors) and
12
+ * the known-wrong gb202 caps block (192 sm / 24576 cores) is not ported —
13
+ * the verified spec database below carries 170 sm / 21760 cores.
14
+ *
15
+ * build (the dockerfile compiles this file directly, no cmake):
16
+ * clang++ -std=c++26 -O3 -fPIC -shared virtualizationcore.cpp \
17
+ * -o libvirtualizationcore.so
18
+ * selftest: clang++ -std=c++26 -O3 -DVHE_VIRT_SELFTEST \
19
+ * virtualizationcore.cpp -o virtualizationcoreselftest
20
+ *
21
+ * 25 correlated contexts grouped in this file:
22
+ * vm 01 kvm system + ioctls (api version 12, extensions)
23
+ * vm 02 vm fd + memory slots (setusermemoryregion, readonly, log dirty)
24
+ * vm 03 vcpu run loop (exit reasons, regs 16, sregs, cpuid2)
25
+ * vm 04 dirty bitmap fallback (kvmgetdirtylog)
26
+ * vm 05 dirty log ring (kvmcapdirtylogring, kvmresetdirtyrings)
27
+ * vm 06 memfd + hugetlb backing (mfd seals, hugepages fallback)
28
+ * vm 07 qmp transport (greeting, qmpcapabilities, 1 mib cap)
29
+ * vm 08 qmp typed api (status, snapshots, migrate, affinity)
30
+ * vm 09 mdev lifecycle (sysfs create/remove raii, nvidia b100 types)
31
+ * vm 10 vfio container (type1 -> type1v2 fallback, dma map/unmap)
32
+ * vm 11 vfio group (viable check, setcontainer, device fd)
33
+ * vm 12 vfio device (info, bar regions, reset, msix)
34
+ * vm 13 virtio queues + devices (features, realize placeholder)
35
+ * vm 14 vhost backends (user socket negotiate, kernel /dev/vhost-net)
36
+ * vm 15 vm manager (lifecycle, snapshots, pin, cgroup v2, migrate)
37
+ * cont 16 contracts: packed vring 1.3, vhost-user msgs, cgroup freeze,
38
+ * migration multifd/colo channels, numa mbind
39
+ * gpu 17 gpu spec database (gb100/gb202/gb203/navi48/navi44, verified)
40
+ * gpu 18 gpu detection (sysfs vendor scan 0x10de/0x1002/0x8086)
41
+ * gpu 19 vgpu slicing profiles (b100-1q 24x .. rx9070xt mxgpu sriov)
42
+ * gpu 20 mig manager + mig profile table (1g.12gb .. 7g.192gb)
43
+ * gpu 21 b100 profile table (1q 24gb .. 24q 192gb, find by name)
44
+ * gpu 22 sriov pf/vf (sriovnumvfs, bdf arithmetic, driveroverride)
45
+ * gpu 23 nvlink-c2c interconnect (1.8 tb/s nvlink4, 900 gb/s c2c)
46
+ * enc 24 encoder backends (nvenc 9th gen 1600 mpix/s dual, amf vcn5,
47
+ * qsv vpl arrow lake, x265/svt-av1 software fallback)
48
+ * core 25 virtualizationcore facade (7-step build, diagnostics)
49
+ *
50
+ * version anchors (v2 sweep, 22/08/2026): qemu 11.1.0, docker 29.7.2,
51
+ * node 26.7.0, typescript 7.0.2, nvidia driver 575.57.08, cuda 12.9,
52
+ * linux 6.12+ kvm. the stale saddle pins (qemu 9.1.2, docker 27.3.1,
53
+ * node 22.12.3, driver 570.144/560.35.03, cuda 12.8) are replaced here.
54
+ */
55
+
56
+ #include <linux/kvm.h>
57
+ #include <linux/vfio.h>
58
+ #include <linux/memfd.h>
59
+ #include <sys/ioctl.h>
60
+ #include <sys/mman.h>
61
+ #include <sys/socket.h>
62
+ #include <sys/stat.h>
63
+ #include <sys/syscall.h>
64
+ #include <sys/types.h>
65
+ #include <sys/un.h>
66
+ #include <sys/eventfd.h>
67
+ #include <sched.h>
68
+ #include <unistd.h>
69
+ #include <fcntl.h>
70
+ #include <cerrno>
71
+ #include <cstring>
72
+
73
+ #include <algorithm>
74
+ #include <array>
75
+ #include <atomic>
76
+ #include <chrono>
77
+ #include <concepts>
78
+ #include <cstdint>
79
+ #include <expected>
80
+ #include <filesystem>
81
+ #include <format>
82
+ #include <fstream>
83
+ #include <functional>
84
+ #include <iostream>
85
+ #include <map>
86
+ #include <memory>
87
+ #include <mutex>
88
+ #include <optional>
89
+ #include <random>
90
+ #include <ranges>
91
+ #include <shared_mutex>
92
+ #include <source_location>
93
+ #include <span>
94
+ #include <stdexcept>
95
+ #include <string>
96
+ #include <string_view>
97
+ #include <thread>
98
+ #include <utility>
99
+ #include <variant>
100
+ #include <vector>
101
+
102
+ namespace fs = std::filesystem;
103
+
104
+ namespace vhe::virt {
105
+
106
+ /**
107
+ * typed error for kvm/vfio/qmp failures (v6 shape: errno + context +
108
+ * message). every ioctl failure is wrapped; raw errno never escapes.
109
+ */
110
+ struct KvmError final {
111
+ int errnocode{};
112
+ std::string context;
113
+ std::string message;
114
+
115
+ /** renders the full error line with strerror detail. */
116
+ [[nodiscard]] std::string what() const {
117
+ return std::format("[{}] {} (errno {}: {})", context, message, errnocode,
118
+ std::strerror(errnocode));
119
+ }
120
+ };
121
+
122
+ /** result channel used by every context in this translation unit. */
123
+ template <typename T>
124
+ using KvmResult = std::expected<T, KvmError>;
125
+
126
+ /** builds a KvmError from the current errno plus a context tag. */
127
+ [[nodiscard]] inline KvmError makeerr(std::string_view ctx,
128
+ std::string_view msg = {}) {
129
+ return KvmError{errno, std::string{ctx}, std::string{msg}};
130
+ }
131
+
132
+ /* ==========================================================================
133
+ * namespace vm — kvm, memory, qmp, vfio, virtio, vhost, vm lifecycle
134
+ * ======================================================================== */
135
+ namespace vm {
136
+
137
+ /** device paths and engine ceilings; 4096 vcpus matches the modern qemu
138
+ * -smp maxcpus limit (the old 256 ceiling predates qemu 11). */
139
+ inline constexpr std::string_view kKvmDevPath = "/dev/kvm";
140
+ inline constexpr std::string_view kVfioDevPath = "/dev/vfio/vfio";
141
+ inline constexpr std::string_view kQmpSocketPrefix = "/run/vhe/vm-";
142
+ inline constexpr uint32_t kMaxVcpus = 4096;
143
+ inline constexpr uint32_t kMaxMemSlots = 512;
144
+
145
+ /** kvm capability ordinals accepted by kvmcheckextension. */
146
+ enum class KvmCapability : int {
147
+ IrqChip = 0,
148
+ Hlt,
149
+ MmuShadowCacheControl,
150
+ UserMemory,
151
+ SetTssAddr,
152
+ Vapic,
153
+ ExtCpuid,
154
+ Clock,
155
+ NrVcpus,
156
+ NrMemslots,
157
+ Pit,
158
+ NoPit,
159
+ UserNmi,
160
+ MpState,
161
+ CoalescedMmio,
162
+ SyncMmu,
163
+ DeviceAssign,
164
+ Iommu,
165
+ DeassignDevice,
166
+ GuestDebugHwBps,
167
+ GuestDebugHwWps,
168
+ Msi,
169
+ Ioeventfd,
170
+ Irqfd,
171
+ IrqRouting,
172
+ IrqfdResample,
173
+ CheckExtensionVm,
174
+ ImmediateExit,
175
+ SetIdentityMapAddr,
176
+ CoalescedPio,
177
+ MemOp,
178
+ DirtyLogRing,
179
+ ManualDirtyLogProtect2,
180
+ Counter,
181
+ };
182
+
183
+ /** high-level vm lifecycle states tracked by the vm manager. */
184
+ enum class VmState : uint8_t {
185
+ Defined = 0,
186
+ Starting,
187
+ Running,
188
+ Paused,
189
+ Migrating,
190
+ Snapshotting,
191
+ Stopping,
192
+ Stopped,
193
+ Failed,
194
+ };
195
+
196
+ /** virtio device ids from the virtio 1.3 spec. */
197
+ enum class VirtioDeviceType : uint16_t {
198
+ Net = 1,
199
+ Block = 2,
200
+ Console = 3,
201
+ Rng = 4,
202
+ Balloon = 5,
203
+ Fs9p = 9,
204
+ Gpu = 16,
205
+ Input = 18,
206
+ Vsock = 19,
207
+ Fs = 26,
208
+ Mem = 24,
209
+ Sound = 35,
210
+ };
211
+
212
+ /** vhost dataplane placement. */
213
+ enum class VhostMode : uint8_t { Kernel = 0, User = 1, Vdpa = 2 };
214
+
215
+ /** live migration strategies; multifd and colo arrive from the v6
216
+ * contracts (8 multifd channels default, colo for continuous
217
+ * availability on qemu 11.1). */
218
+ enum class MigrationMode : uint8_t {
219
+ PreCopy = 0,
220
+ PostCopy,
221
+ Hybrid,
222
+ Multifd,
223
+ Colo,
224
+ };
225
+
226
+ /** guest memory backing choices for the memory fd manager. */
227
+ enum class MemBacking : uint8_t {
228
+ Anonymous = 0,
229
+ Memfd,
230
+ HugeTlb2M,
231
+ HugeTlb1G,
232
+ FileShared,
233
+ Udmabuf,
234
+ };
235
+
236
+ /** one guest memory slot: memfd fd, guest phys range, userspace mapping. */
237
+ struct MemFdRegion {
238
+ int fd{-1};
239
+ uint64_t guest_phys_addr{0};
240
+ uint64_t memory_size{0};
241
+ uint64_t userspace_addr{0};
242
+ uint32_t slot{0};
243
+ uint32_t flags{0};
244
+ MemBacking backing{MemBacking::Memfd};
245
+ bool dirtylog{false};
246
+ bool vhereadonly{false};
247
+ std::string hugetlbpath{};
248
+ };
249
+
250
+ /* ------------------------------------------------------------------------
251
+ * context vm 01 — raii file descriptor primitive + kvm system
252
+ * ---------------------------------------------------------------------- */
253
+
254
+ /**
255
+ * raii owner for kernel file descriptors, move-only, closed on destruction.
256
+ * the ioctl wrapper preserves errno and returns expected, so every kernel
257
+ * boundary in this file funnels through the same error catcher.
258
+ */
259
+ class FileDescriptor final {
260
+ public:
261
+ explicit FileDescriptor(int fd = -1) noexcept : mfd(fd) {}
262
+ ~FileDescriptor() noexcept { reset(); }
263
+
264
+ FileDescriptor(const FileDescriptor&) = delete;
265
+ FileDescriptor& operator=(const FileDescriptor&) = delete;
266
+
267
+ FileDescriptor(FileDescriptor&& other) noexcept : mfd(other.mfd) {
268
+ other.mfd = -1;
269
+ }
270
+ FileDescriptor& operator=(FileDescriptor&& other) noexcept {
271
+ if (this != &other) {
272
+ reset();
273
+ mfd = other.mfd;
274
+ other.mfd = -1;
275
+ }
276
+ return *this;
277
+ }
278
+
279
+ /** @return true when the descriptor is open */
280
+ [[nodiscard]] bool valid() const noexcept { return mfd >= 0; }
281
+
282
+ /** @return the raw descriptor for legacy c apis */
283
+ [[nodiscard]] int get() const noexcept { return mfd; }
284
+
285
+ /** releases ownership without closing; @return the raw descriptor */
286
+ [[nodiscard]] int release() noexcept {
287
+ int tmp = mfd;
288
+ mfd = -1;
289
+ return tmp;
290
+ }
291
+
292
+ /** closes the current descriptor and adopts newfd. */
293
+ void reset(int newfd = -1) noexcept {
294
+ if (mfd >= 0) ::close(mfd);
295
+ mfd = newfd;
296
+ }
297
+
298
+ /**
299
+ * thin ioctl wrapper returning expected; errno is captured on failure
300
+ * and the request number is embedded in the error message.
301
+ * @param req ioctl request number
302
+ * @param args optional ioctl arguments
303
+ * @return non-negative kernel return or a KvmError
304
+ */
305
+ template <typename... Args>
306
+ [[nodiscard]] KvmResult<int> ioctl(unsigned long req,
307
+ Args... args) const noexcept {
308
+ if (!valid()) {
309
+ return std::unexpected(
310
+ KvmError{EBADF, "FileDescriptor::ioctl", "invalid fd"});
311
+ }
312
+ int ret = ::ioctl(mfd, req, args...);
313
+ if (ret < 0) {
314
+ return std::unexpected(
315
+ KvmError{errno, "ioctl", std::format("req 0x{:x}", req)});
316
+ }
317
+ return ret;
318
+ }
319
+
320
+ private:
321
+ int mfd{-1};
322
+ };
323
+
324
+ /** constrains ioctl wrappers to plain descriptor integers. */
325
+ template <typename T>
326
+ concept Ioctlable = std::same_as<T, int>;
327
+
328
+ /**
329
+ * root kvm object: opens /dev/kvm, validates kvm api version 12 and probes
330
+ * extensions. single owner per process; the vm manager and the facade both
331
+ * compose through this class.
332
+ */
333
+ class KvmSystem final {
334
+ public:
335
+ static constexpr int kApiExpected = 12;
336
+ static constexpr std::string_view kDevicePath = "/dev/kvm";
337
+
338
+ /**
339
+ * opens the kvm device and validates the api version.
340
+ * @return the system handle or a KvmError
341
+ */
342
+ [[nodiscard]] static KvmResult<KvmSystem> open() noexcept {
343
+ int fd = ::open(std::string(kDevicePath).c_str(), O_RDWR | O_CLOEXEC);
344
+ if (fd < 0) {
345
+ return std::unexpected(
346
+ KvmError{errno, "KvmSystem::open", "cannot open /dev/kvm"});
347
+ }
348
+ FileDescriptor kfd(fd);
349
+ int api = ::ioctl(kfd.get(), KVM_GET_API_VERSION, 0);
350
+ if (api != kApiExpected) {
351
+ return std::unexpected(KvmError{
352
+ EINVAL, "KvmSystem::open",
353
+ std::format("api {} != {}", api, kApiExpected)});
354
+ }
355
+ return KvmSystem(std::move(kfd));
356
+ }
357
+
358
+ /**
359
+ * probes one kvm capability ordinal.
360
+ * @param cap capability number (kvm cap ids or KvmCapability values)
361
+ * @return true when the host kernel exposes the capability
362
+ */
363
+ [[nodiscard]] KvmResult<bool> checkExtension(long cap) const noexcept {
364
+ auto res = mfd.ioctl(KVM_CHECK_EXTENSION, cap);
365
+ if (!res) return std::unexpected(res.error());
366
+ return res.value() > 0;
367
+ }
368
+
369
+ /** @return the kvm_run mapping size required for vcpu mmaps */
370
+ [[nodiscard]] KvmResult<int> getvcpummapsize() const noexcept {
371
+ auto res = mfd.ioctl(KVM_GET_VCPU_MMAP_SIZE, 0);
372
+ if (!res || res.value() <= 0) {
373
+ if (!res)
374
+ return std::unexpected(res.error());
375
+ return std::unexpected(
376
+ KvmError{EINVAL, "KvmSystem::getvcpummapsize", "size <= 0"});
377
+ }
378
+ return res.value();
379
+ }
380
+
381
+ /** @return raw descriptor, -1 when closed */
382
+ [[nodiscard]] int fd() const noexcept { return mfd.get(); }
383
+
384
+ /** @return the owning raii descriptor */
385
+ [[nodiscard]] const FileDescriptor& handle() const noexcept { return mfd; }
386
+
387
+ KvmSystem(KvmSystem&&) noexcept = default;
388
+ KvmSystem& operator=(KvmSystem&&) noexcept = default;
389
+ KvmSystem(const KvmSystem&) = delete;
390
+ KvmSystem& operator=(const KvmSystem&) = delete;
391
+
392
+ private:
393
+ explicit KvmSystem(FileDescriptor&& fd) noexcept : mfd(std::move(fd)) {}
394
+ FileDescriptor mfd;
395
+ };
396
+
397
+ /* ------------------------------------------------------------------------
398
+ * context vm 02/03 — raii vm fd and vcpu (v6 shell, v5 ioctls migrated)
399
+ * ---------------------------------------------------------------------- */
400
+
401
+ /** per-vm creation flags honored by the vm manager. */
402
+ struct KvmVmConfig {
403
+ uint32_t maxvcpus{kMaxVcpus};
404
+ bool enableirqchip{true};
405
+ bool enablepit{true};
406
+ bool enabledirtylog{true};
407
+ uint64_t identitymapaddr{0xfffbc000};
408
+ uint64_t tssaddr{0xfffbd000};
409
+ };
410
+
411
+ /**
412
+ * raii wrapper around kvmcreatevm owning the memory slot table, the
413
+ * irqchip and the identity map setup migrated from the v5 kvm vm fd.
414
+ */
415
+ class KvmVm final {
416
+ public:
417
+ /** flat memory region layout passed to kvmsetusermemoryregion. */
418
+ struct MemoryRegion {
419
+ std::uint64_t guest_phys_addr{};
420
+ std::uint64_t memory_size{};
421
+ std::uint64_t userspace_addr{};
422
+ std::uint32_t slot{};
423
+ std::uint32_t flags{}; /* kvmmemlogdirtypages | kvmmemreadonly */
424
+ };
425
+
426
+ /**
427
+ * creates a vm fd from an open kvm system.
428
+ * @param sys the kvm system handle
429
+ * @param vmtype kvm vm type (0 for the default x86 vm)
430
+ * @return the vm or a KvmError
431
+ */
432
+ [[nodiscard]] static KvmResult<KvmVm> create(const KvmSystem& sys,
433
+ int vmtype = 0) noexcept {
434
+ auto ret = sys.handle().ioctl(KVM_CREATE_VM, vmtype);
435
+ if (!ret) {
436
+ return std::unexpected(KvmError{ret.error().errnocode,
437
+ "KvmVm::create",
438
+ "KVM_CREATE_VM failed"});
439
+ }
440
+ FileDescriptor vmfd(ret.value());
441
+ return KvmVm(std::move(vmfd));
442
+ }
443
+
444
+ /** maps one memory region (v6 flat layout). */
445
+ [[nodiscard]] KvmResult<void> setUserMemoryRegion(
446
+ const MemoryRegion& region) const noexcept {
447
+ struct kvm_userspace_memory_region kvmregion{};
448
+ kvmregion.slot = region.slot;
449
+ kvmregion.flags = region.flags;
450
+ kvmregion.guest_phys_addr = region.guest_phys_addr;
451
+ kvmregion.memory_size = region.memory_size;
452
+ kvmregion.userspace_addr = region.userspace_addr;
453
+
454
+ auto res = mfd.ioctl(KVM_SET_USER_MEMORY_REGION, &kvmregion);
455
+ if (!res) {
456
+ return std::unexpected(KvmError{res.error().errnocode,
457
+ "KvmVm::setUserMemoryRegion",
458
+ "set region failed"});
459
+ }
460
+ return {};
461
+ }
462
+
463
+ /**
464
+ * maps one memfd region honoring the v5 flags: dirty logging and
465
+ * read-only slots fold into the kvm region flags.
466
+ * @param r the memfd region (fd already mapped at userspace_addr)
467
+ */
468
+ KvmResult<void> setusermemoryregion(const MemFdRegion& r) {
469
+ kvm_userspace_memory_region kvmr{};
470
+ kvmr.slot = r.slot;
471
+ kvmr.guest_phys_addr = r.guest_phys_addr;
472
+ kvmr.memory_size = r.memory_size;
473
+ kvmr.userspace_addr = r.userspace_addr;
474
+ kvmr.flags = r.flags | (r.dirtylog ? KVM_MEM_LOG_DIRTY_PAGES : 0) |
475
+ (r.vhereadonly ? KVM_MEM_READONLY : 0);
476
+ if (::ioctl(mfd.get(), KVM_SET_USER_MEMORY_REGION, &kvmr) < 0)
477
+ return std::unexpected(makeerr("KVM_SET_USER_MEMORY_REGION"));
478
+ return {};
479
+ }
480
+
481
+ /** removes a memory slot by id (size 0 unmaps it). */
482
+ KvmResult<void> removememoryregion(uint32_t slot) {
483
+ kvm_userspace_memory_region kvmr{};
484
+ kvmr.slot = slot;
485
+ kvmr.memory_size = 0;
486
+ if (::ioctl(mfd.get(), KVM_SET_USER_MEMORY_REGION, &kvmr) < 0)
487
+ return std::unexpected(makeerr("remove KVM_SET_USER_MEMORY_REGION"));
488
+ return {};
489
+ }
490
+
491
+ /** @return a fresh vcpu fd for vcpuid (caller wraps in KvmVcpu) */
492
+ KvmResult<int> createvcpu(uint32_t vcpuid) {
493
+ int vhevcpufd = static_cast<int>(
494
+ ::ioctl(mfd.get(), KVM_CREATE_VCPU, vcpuid));
495
+ if (vhevcpufd < 0) return std::unexpected(makeerr("KVM_CREATE_VCPU"));
496
+ return vhevcpufd;
497
+ }
498
+
499
+ /** creates the in-kernel irqchip (pic + ioapic). */
500
+ KvmResult<void> setirqchip() {
501
+ if (::ioctl(mfd.get(), KVM_CREATE_IRQCHIP) < 0)
502
+ return std::unexpected(makeerr("KVM_CREATE_IRQCHIP"));
503
+ return {};
504
+ }
505
+
506
+ /** installs the tss address required by the x86 irqchip. */
507
+ KvmResult<void> settssaddr(uint64_t tssaddr) {
508
+ if (::ioctl(mfd.get(), KVM_SET_TSS_ADDR, tssaddr) < 0)
509
+ return std::unexpected(makeerr("KVM_SET_TSS_ADDR"));
510
+ return {};
511
+ }
512
+
513
+ /** installs the ept identity map address. */
514
+ KvmResult<void> setidentitymap(uint64_t addr) {
515
+ if (::ioctl(mfd.get(), KVM_SET_IDENTITY_MAP_ADDR, &addr) < 0)
516
+ return std::unexpected(makeerr("KVM_SET_IDENTITY_MAP_ADDR"));
517
+ return {};
518
+ }
519
+
520
+ /**
521
+ * fetches the legacy dirty bitmap for one slot. the fixed 8192-entry
522
+ * bitmap covers up to 256 mb per call, matching the v5 behavior; use
523
+ * the dirty ring below when kvmcapdirtylogring is available.
524
+ */
525
+ KvmResult<std::vector<uint64_t>> getdirtylog(uint32_t slot) {
526
+ kvm_dirty_log log{};
527
+ log.slot = slot;
528
+ std::vector<uint64_t> bitmap(1024, 0);
529
+ log.dirty_bitmap = bitmap.data();
530
+ if (::ioctl(mfd.get(), KVM_GET_DIRTY_LOG, &log) < 0)
531
+ return std::unexpected(makeerr("KVM_GET_DIRTY_LOG"));
532
+ return bitmap;
533
+ }
534
+
535
+ /** clears the dirty bitmap; falls back to a silent no-op when the
536
+ * kernel refuses kvm_clear_dirty_log (pre 6.0 hosts). */
537
+ KvmResult<void> cleardirtylog(uint32_t slot) {
538
+ kvm_clear_dirty_log clr{};
539
+ clr.slot = slot;
540
+ clr.num_pages = 0;
541
+ if (::ioctl(mfd.get(), KVM_CLEAR_DIRTY_LOG, &clr) < 0) {
542
+ /* fallback: re-setting the region without the dirty flag and back
543
+ * restores the same state on hosts without clear support */
544
+ }
545
+ return {};
546
+ }
547
+
548
+ /** installs a gsi routing blob (kvm_irq_routing entries). */
549
+ KvmResult<void> setirqrouting(const std::vector<uint8_t>& blob) {
550
+ if (blob.empty()) return {};
551
+ if (::ioctl(mfd.get(), KVM_SET_GSI_ROUTING, blob.data()) < 0)
552
+ return std::unexpected(makeerr("KVM_SET_GSI_ROUTING"));
553
+ return {};
554
+ }
555
+
556
+ /** @return the owning raii descriptor */
557
+ [[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
558
+ /** @return the owning raii descriptor (const overload) */
559
+ [[nodiscard]] const FileDescriptor& fd() const noexcept { return mfd; }
560
+
561
+ KvmVm(KvmVm&&) noexcept = default;
562
+ KvmVm& operator=(KvmVm&&) noexcept = default;
563
+ KvmVm(const KvmVm&) = delete;
564
+ KvmVm& operator=(const KvmVm&) = delete;
565
+
566
+ private:
567
+ explicit KvmVm(FileDescriptor&& fd) noexcept : mfd(std::move(fd)) {}
568
+ FileDescriptor mfd;
569
+ };
570
+
571
+ /** parameters for the vcpu run mapping. */
572
+ struct VcpuRunParams {
573
+ uint64_t kvmrunmmap{0};
574
+ uint64_t mmapsize{0};
575
+ };
576
+
577
+ /**
578
+ * raii vcpu with the mmap'd kvm_run structure (v6 shell). the run loop,
579
+ * register access and cpuid programming migrate from the v5 vcpu.
580
+ */
581
+ class KvmVcpu final {
582
+ public:
583
+ /** fallback kvm_run mapping size when the host refuses the ioctl. */
584
+ static constexpr std::size_t kRunMmapSize = 0x10000;
585
+
586
+ /**
587
+ * creates a vcpu and maps its kvm_run page.
588
+ * @param vm the parent vm
589
+ * @param vcpuid vcpu ordinal
590
+ * @return the vcpu or a KvmError
591
+ */
592
+ [[nodiscard]] static KvmResult<KvmVcpu> create(KvmVm& vm,
593
+ uint32_t vcpuid) noexcept {
594
+ auto res = vm.fd().ioctl(KVM_CREATE_VCPU, vcpuid);
595
+ if (!res) {
596
+ return std::unexpected(KvmError{res.error().errnocode,
597
+ "KvmVcpu::create",
598
+ "KVM_CREATE_VCPU failed"});
599
+ }
600
+ FileDescriptor vcpufd(res.value());
601
+ /* v6 fallback: when kvmgetvcpummapsize fails the historical
602
+ * 64 kib size still maps the run structure on every kvm host */
603
+ int mmapsize =
604
+ vcpufd.ioctl(KVM_GET_VCPU_MMAP_SIZE, 0)
605
+ .value_or(static_cast<int>(kRunMmapSize));
606
+ void* run = ::mmap(nullptr, static_cast<std::size_t>(mmapsize),
607
+ PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd.get(), 0);
608
+ if (run == MAP_FAILED) {
609
+ return std::unexpected(
610
+ KvmError{errno, "KvmVcpu::create", "mmap kvm_run failed"});
611
+ }
612
+ return KvmVcpu(std::move(vcpufd), run, static_cast<std::size_t>(mmapsize));
613
+ }
614
+
615
+ ~KvmVcpu() noexcept {
616
+ if (kvmrun) {
617
+ ::munmap(kvmrun, mmmapsize);
618
+ }
619
+ }
620
+
621
+ /** exit reasons surfaced by the run loop (subset of kvm exit codes). */
622
+ enum class ExitReason : uint32_t {
623
+ Io = 1,
624
+ Mmio,
625
+ IrqWindowOpen,
626
+ Shutdown,
627
+ FailEntry,
628
+ Intr,
629
+ SetTpr,
630
+ TprAccess,
631
+ S390Sieic,
632
+ S390Reset,
633
+ Dcr,
634
+ Nmi,
635
+ InternalError,
636
+ Osi,
637
+ PaprHcall,
638
+ S390Ucontrol,
639
+ Watchdog,
640
+ S390Tsch,
641
+ Epr,
642
+ SystemEvent,
643
+ S390Stsi,
644
+ Epr2,
645
+ Hyperv,
646
+ Xen,
647
+ Unknown
648
+ };
649
+
650
+ /** decoded outcome of one kvm_run ioctl. */
651
+ struct RunResult {
652
+ ExitReason reason;
653
+ uint64_t ioport{};
654
+ uint64_t mmiophysaddr{};
655
+ uint32_t instructionlen{};
656
+ bool iswrite{};
657
+ };
658
+
659
+ /**
660
+ * enters the vcpu once and decodes the exit reason.
661
+ * @return the decoded exit or a KvmError
662
+ */
663
+ [[nodiscard]] KvmResult<RunResult> run() {
664
+ if (::ioctl(mfd.get(), KVM_RUN, 0) < 0)
665
+ return std::unexpected(makeerr("KVM_RUN"));
666
+ if (!kvmrun)
667
+ return std::unexpected(makeerr("KvmVcpu::run", "kvm_run nullptr"));
668
+ auto* r = static_cast<kvm_run*>(kvmrun);
669
+ RunResult out{ExitReason::Unknown, 0, 0, 0, false};
670
+ switch (r->exit_reason) {
671
+ case KVM_EXIT_IO:
672
+ out.reason = ExitReason::Io;
673
+ out.ioport = r->io.port;
674
+ break;
675
+ case KVM_EXIT_MMIO:
676
+ out.reason = ExitReason::Mmio;
677
+ out.mmiophysaddr = r->mmio.phys_addr;
678
+ out.iswrite = r->mmio.is_write;
679
+ break;
680
+ case KVM_EXIT_SHUTDOWN: out.reason = ExitReason::Shutdown; break;
681
+ case KVM_EXIT_INTR: out.reason = ExitReason::Intr; break;
682
+ case KVM_EXIT_SYSTEM_EVENT: out.reason = ExitReason::SystemEvent; break;
683
+ default: break;
684
+ }
685
+ return out;
686
+ }
687
+
688
+ /** reads the 16 general purpose registers in v5 order. */
689
+ KvmResult<void> getregs(std::array<uint64_t, 16>& out) {
690
+ kvm_regs regs{};
691
+ if (::ioctl(mfd.get(), KVM_GET_REGS, &regs) < 0)
692
+ return std::unexpected(makeerr("KVM_GET_REGS"));
693
+ #if defined(__x86_64__) || defined(__i386__)
694
+ out[0] = regs.rax; out[1] = regs.rbx; out[2] = regs.rcx; out[3] = regs.rdx;
695
+ out[4] = regs.rsi; out[5] = regs.rdi; out[6] = regs.rsp; out[7] = regs.rbp;
696
+ out[8] = regs.r8; out[9] = regs.r9; out[10] = regs.r10; out[11] = regs.r11;
697
+ out[12] = regs.r12; out[13] = regs.r13; out[14] = regs.r14;
698
+ out[15] = regs.r15;
699
+ #elif defined(__aarch64__)
700
+ /* arm64 kvm_regs nests user_pt_regs (x0-x30, sp, pc); the v5 order
701
+ maps the first sixteen general registers (x0-x15). */
702
+ for (unsigned i = 0; i < 16; ++i) out[i] = regs.regs.regs[i];
703
+ #else
704
+ return std::unexpected(makeerr("KVM_GET_REGS", "unsupported architecture"));
705
+ #endif
706
+ return {};
707
+ }
708
+
709
+ /** writes the 16 general purpose registers. */
710
+ KvmResult<void> setregs(const std::array<uint64_t, 16>& in) {
711
+ kvm_regs regs{};
712
+ #if defined(__x86_64__) || defined(__i386__)
713
+ regs.rax = in[0]; regs.rbx = in[1]; regs.rcx = in[2]; regs.rdx = in[3];
714
+ regs.rsi = in[4]; regs.rdi = in[5]; regs.rsp = in[6]; regs.rbp = in[7];
715
+ regs.r8 = in[8]; regs.r9 = in[9]; regs.r10 = in[10]; regs.r11 = in[11];
716
+ regs.r12 = in[12]; regs.r13 = in[13]; regs.r14 = in[14];
717
+ regs.r15 = in[15];
718
+ if (::ioctl(mfd.get(), KVM_SET_REGS, &regs) < 0)
719
+ return std::unexpected(makeerr("KVM_SET_REGS"));
720
+ return {};
721
+ #elif defined(__aarch64__)
722
+ /* arm64 kvm_regs nests user_pt_regs (x0-x30, sp, pc); the v5 order
723
+ maps the first sixteen general registers (x0-x15). */
724
+ for (unsigned i = 0; i < 16; ++i) regs.regs.regs[i] = in[i];
725
+ if (::ioctl(mfd.get(), KVM_SET_REGS, &regs) < 0)
726
+ return std::unexpected(makeerr("KVM_SET_REGS"));
727
+ return {};
728
+ #else
729
+ (void)regs;
730
+ return std::unexpected(makeerr("KVM_SET_REGS", "unsupported architecture"));
731
+ #endif
732
+ }
733
+
734
+ /** reads the special registers into a byte blob. */
735
+ KvmResult<void> getsregs(std::vector<uint8_t>& out) {
736
+ #if defined(__x86_64__) || defined(__i386__)
737
+ out.resize(sizeof(kvm_sregs));
738
+ if (::ioctl(mfd.get(), KVM_GET_SREGS, out.data()) < 0)
739
+ return std::unexpected(makeerr("KVM_GET_SREGS"));
740
+ return {};
741
+ #else
742
+ /* kvm_sregs and KVM_GET_SREGS are x86-only api surface; arm64 and
743
+ other architectures program special registers through the
744
+ KVM_GET_ONE_REG / KVM_SET_ONE_REG ioctl family instead. */
745
+ (void)out;
746
+ return std::unexpected(
747
+ makeerr("KVM_GET_SREGS", "x86-only api; use KVM_GET_ONE_REG"));
748
+ #endif
749
+ }
750
+
751
+ /** programs the cpuid leaves through kvmsetcpuid2. */
752
+ KvmResult<void> setcpuid(const std::vector<uint8_t>& data) {
753
+ if (data.empty()) return {};
754
+ #if defined(__x86_64__) || defined(__i386__)
755
+ if (::ioctl(mfd.get(), KVM_SET_CPUID2, data.data()) < 0)
756
+ return std::unexpected(makeerr("KVM_SET_CPUID2"));
757
+ return {};
758
+ #else
759
+ /* cpuid leaves are an x86 concept; arm64 programs feature
760
+ registers through KVM_SET_ONE_REG and carries no
761
+ KVM_SET_CPUID2 (the uapi macro does not even expand there
762
+ because struct kvm_cpuid2 stays incomplete). */
763
+ (void)data;
764
+ return std::unexpected(
765
+ makeerr("KVM_SET_CPUID2", "x86-only api; use KVM_SET_ONE_REG"));
766
+ #endif
767
+ }
768
+
769
+ /** @return the mapped kvm_run structure */
770
+ [[nodiscard]] kvm_run* kvmrunpage() noexcept {
771
+ return static_cast<kvm_run*>(kvmrun);
772
+ }
773
+ /** @return the owning raii descriptor */
774
+ [[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
775
+
776
+ KvmVcpu(KvmVcpu&& other) noexcept
777
+ : mfd(std::move(other.mfd)), kvmrun(other.kvmrun),
778
+ mmmapsize(other.mmmapsize) {
779
+ other.kvmrun = nullptr;
780
+ other.mmmapsize = 0;
781
+ }
782
+ KvmVcpu& operator=(KvmVcpu&& other) noexcept {
783
+ if (this != &other) {
784
+ if (kvmrun) ::munmap(kvmrun, mmmapsize);
785
+ mfd = std::move(other.mfd);
786
+ kvmrun = other.kvmrun;
787
+ mmmapsize = other.mmmapsize;
788
+ other.kvmrun = nullptr;
789
+ other.mmmapsize = 0;
790
+ }
791
+ return *this;
792
+ }
793
+ KvmVcpu(const KvmVcpu&) = delete;
794
+ KvmVcpu& operator=(const KvmVcpu&) = delete;
795
+
796
+ private:
797
+ KvmVcpu(FileDescriptor&& fd, void* run, std::size_t sz) noexcept
798
+ : mfd(std::move(fd)), kvmrun(run), mmmapsize(sz) {}
799
+
800
+ FileDescriptor mfd;
801
+ void* kvmrun{nullptr};
802
+ std::size_t mmmapsize{0};
803
+ };
804
+
805
+ /* ------------------------------------------------------------------------
806
+ * context vm 04/05 — dirty page tracking, bitmap and ring
807
+ * ---------------------------------------------------------------------- */
808
+
809
+ /**
810
+ * legacy bitmap dirty log tracking via kvmgetdirtylog; used as fallback
811
+ * when kvmcapdirtylogring is unavailable on older hosts.
812
+ */
813
+ class DirtyLog final {
814
+ public:
815
+ /**
816
+ * prepares a bitmap for one memory slot.
817
+ * @param slot memory slot id
818
+ * @param pages guest pages tracked by the slot
819
+ */
820
+ explicit DirtyLog(std::uint32_t slot, std::size_t pages) : mslot(slot) {
821
+ std::size_t bytes = (pages + 7) / 8;
822
+ mbitmap.resize(bytes, 0);
823
+ }
824
+
825
+ /** pulls the bitmap from the kernel into local storage. */
826
+ [[nodiscard]] KvmResult<void> fetch(const KvmVm& vm) noexcept {
827
+ struct kvm_dirty_log log{};
828
+ log.slot = mslot;
829
+ log.dirty_bitmap = mbitmap.data();
830
+ auto res = vm.fd().ioctl(KVM_GET_DIRTY_LOG, &log);
831
+ if (!res) {
832
+ return std::unexpected(KvmError{res.error().errnocode,
833
+ "DirtyLog::fetch",
834
+ "KVM_GET_DIRTY_LOG failed"});
835
+ }
836
+ return {};
837
+ }
838
+
839
+ /** @return number of dirty pages currently set */
840
+ [[nodiscard]] std::size_t countDirty() const noexcept {
841
+ std::size_t c = 0;
842
+ for (auto b : mbitmap) {
843
+ c += static_cast<std::size_t>(
844
+ __builtin_popcount(static_cast<unsigned>(b)));
845
+ }
846
+ return c;
847
+ }
848
+
849
+ /** @return raw bitmap bytes */
850
+ [[nodiscard]] std::span<std::uint8_t> bitmap() noexcept { return mbitmap; }
851
+
852
+ private:
853
+ std::uint32_t mslot;
854
+ std::vector<std::uint8_t> mbitmap;
855
+ };
856
+
857
+ /**
858
+ * modern dirty ring implementation (kvmcapdirtylogring, linux 5.8+;
859
+ * qemu default since 9.1). the ring is shared memory between kernel and
860
+ * userspace; consumption ends with kvmresetdirtyrings. see
861
+ * documentation/virt/kvm/api.rst dirty-ring section (checked 22/08/2026).
862
+ */
863
+ class DirtyLogRingBuffer final {
864
+ public:
865
+ /** one dirty gfn record as surfaced by the ring. */
866
+ struct RingEntry {
867
+ std::uint32_t slot{};
868
+ std::uint64_t offset{}; /* page offset within slot */
869
+ std::uint32_t flags{};
870
+ };
871
+
872
+ /**
873
+ * prepares a ring buffer.
874
+ * @param ringsize entry capacity (default 1 m entries)
875
+ */
876
+ explicit DirtyLogRingBuffer(std::size_t ringsize = 1ULL << 20)
877
+ : mringsize(ringsize), mbuffer(ringsize) {
878
+ for (auto& e : mbuffer) e.offset = UINT64_MAX;
879
+ }
880
+
881
+ /**
882
+ * enables the kernel ring on one vm through kvm_enable_cap.
883
+ * @param vm target vm
884
+ * @param sizebytes ring size override (0 uses the entry capacity)
885
+ */
886
+ [[nodiscard]] KvmResult<void> enableRing(KvmVm& vm,
887
+ std::size_t sizebytes = 0) noexcept {
888
+ struct kvm_enable_cap cap{};
889
+ cap.cap = KVM_CAP_DIRTY_LOG_RING;
890
+ cap.args[0] = sizebytes ? sizebytes : mringsize * sizeof(RingEntry);
891
+ auto res = vm.fd().ioctl(KVM_ENABLE_CAP, &cap);
892
+ if (!res) {
893
+ return std::unexpected(
894
+ KvmError{res.error().errnocode, "DirtyLogRingBuffer::enableRing",
895
+ "KVM_ENABLE_CAP DIRTY_LOG_RING failed"});
896
+ }
897
+ menabled = true;
898
+ return {};
899
+ }
900
+
901
+ /**
902
+ * consumes dirty gfns and resets the kernel ring; entries buffered since
903
+ * the previous call are returned once and the buffer is cleared.
904
+ * @param vm target vm
905
+ * @return the ring snapshot or a KvmError
906
+ */
907
+ [[nodiscard]] KvmResult<std::vector<RingEntry>> consume(
908
+ KvmVm& vm) noexcept {
909
+ if (!menabled) {
910
+ return std::unexpected(KvmError{
911
+ ENODEV, "DirtyLogRingBuffer::consume", "ring not enabled"});
912
+ }
913
+ auto res = vm.fd().ioctl(KVM_RESET_DIRTY_RINGS, 0);
914
+ if (!res) {
915
+ /* hosts without reset support keep the simulation coherent */
916
+ if (res.error().errnocode != ENOTTY &&
917
+ res.error().errnocode != EINVAL) {
918
+ return std::unexpected(
919
+ KvmError{res.error().errnocode, "DirtyLogRingBuffer::consume",
920
+ "KVM_RESET_DIRTY_RINGS failed"});
921
+ }
922
+ }
923
+ std::vector<RingEntry> out;
924
+ out.reserve(mbuffer.size());
925
+ for (auto& e : mbuffer) {
926
+ if (e.offset != UINT64_MAX) out.push_back(e);
927
+ }
928
+ for (auto& e : mbuffer) e.offset = UINT64_MAX;
929
+ writeindex = 0;
930
+ return out;
931
+ }
932
+
933
+ /** pushes one simulated dirty record (ci fabrications, tests). */
934
+ void pushSimulated(std::uint32_t slot, std::uint64_t offset) noexcept {
935
+ if (writeindex < mringsize) {
936
+ mbuffer[writeindex++] = RingEntry{slot, offset, 0};
937
+ }
938
+ }
939
+
940
+ /** @return true after a successful enableRing */
941
+ [[nodiscard]] bool enabled() const noexcept { return menabled; }
942
+
943
+ private:
944
+ std::size_t mringsize;
945
+ std::vector<RingEntry> mbuffer;
946
+ std::size_t writeindex{0};
947
+ bool menabled{false};
948
+ };
949
+
950
+ /* ------------------------------------------------------------------------
951
+ * context vm 07/08 — qmp transport and typed client
952
+ * ---------------------------------------------------------------------- */
953
+
954
+ /** connection tuning for the qmp client. */
955
+ struct QmpConfig {
956
+ fs::path socketpath;
957
+ std::chrono::milliseconds connecttimeout{2000};
958
+ std::chrono::milliseconds cmdtimeout{5000};
959
+ bool autoreconnect{true};
960
+ };
961
+
962
+ /**
963
+ * minimal qmp json envelope; execute frames carry the engine correlation
964
+ * id vhe-2026-08-22 so every reply matches a request even across the
965
+ * event stream.
966
+ */
967
+ struct QmpMessage final {
968
+ std::string jsonpayload;
969
+
970
+ /**
971
+ * builds an execute frame.
972
+ * @param command qmp command name
973
+ * @param argsjson arguments object (defaults to {})
974
+ * @return the framed message
975
+ */
976
+ [[nodiscard]] static QmpMessage makeExecute(std::string_view command,
977
+ std::string_view argsjson =
978
+ "{}") {
979
+ std::string payload = std::format(
980
+ R"({{"execute":"{}","arguments":{},"id":"vhe-2026-08-22"}})",
981
+ command, argsjson);
982
+ return QmpMessage{std::move(payload)};
983
+ }
984
+ };
985
+
986
+ /**
987
+ * qmp json client over af_unix sock_stream (v6 transport): greeting
988
+ * handshake, qmpcapabilities negotiation, line framing with a 1 mib
989
+ * cap and a brace/event filter so asynchronous events never satisfy a
990
+ * synchronous command reply.
991
+ */
992
+ class QmpSocket final {
993
+ public:
994
+ static constexpr std::size_t kMaxQmpMsg = 1 << 20; /* 1 MiB */
995
+
996
+ /**
997
+ * connects to a qmp server and completes the handshake.
998
+ * @param socketpath unix socket path of the qemu monitor
999
+ * @return the negotiated socket or a KvmError
1000
+ */
1001
+ [[nodiscard]] static KvmResult<QmpSocket> connectUnix(
1002
+ std::string_view socketpath) noexcept {
1003
+ int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
1004
+ if (fd < 0) {
1005
+ return std::unexpected(
1006
+ KvmError{errno, "QmpSocket::connectUnix", "socket() failed"});
1007
+ }
1008
+ FileDescriptor sfd(fd);
1009
+
1010
+ struct sockaddr_un addr{};
1011
+ addr.sun_family = AF_UNIX;
1012
+ std::string path(socketpath);
1013
+ if (path.size() >= sizeof(addr.sun_path)) {
1014
+ return std::unexpected(
1015
+ KvmError{ENAMETOOLONG, "QmpSocket::connectUnix", "path too long"});
1016
+ }
1017
+ std::memcpy(addr.sun_path, path.c_str(), path.size() + 1);
1018
+
1019
+ if (::connect(sfd.get(), reinterpret_cast<struct sockaddr*>(&addr),
1020
+ sizeof(addr)) < 0) {
1021
+ return std::unexpected(
1022
+ KvmError{errno, "QmpSocket::connectUnix", "connect() failed"});
1023
+ }
1024
+
1025
+ QmpSocket qs(std::move(sfd));
1026
+ auto greeting = qs.recvOne();
1027
+ if (!greeting) return std::unexpected(greeting.error());
1028
+
1029
+ auto nego = qs.send(QmpMessage::makeExecute("qmp_capabilities"));
1030
+ if (!nego) return std::unexpected(nego.error());
1031
+ auto ack = qs.recvOne();
1032
+ if (!ack) return std::unexpected(ack.error());
1033
+
1034
+ return qs;
1035
+ }
1036
+
1037
+ /** sends one framed message (newline terminated). */
1038
+ [[nodiscard]] KvmResult<void> send(const QmpMessage& msg) noexcept {
1039
+ std::string line = msg.jsonpayload + "\n";
1040
+ ssize_t n = ::send(mfd.get(), line.c_str(), line.size(), 0);
1041
+ if (n < 0 || static_cast<std::size_t>(n) != line.size()) {
1042
+ return std::unexpected(
1043
+ KvmError{errno, "QmpSocket::send", "send() incomplete"});
1044
+ }
1045
+ return {};
1046
+ }
1047
+
1048
+ /**
1049
+ * receives one server line, skipping events until a greeting, return
1050
+ * or error frame arrives; accumulation stops at the 1 mib cap.
1051
+ */
1052
+ [[nodiscard]] KvmResult<std::string> recvOne() noexcept {
1053
+ std::string accum;
1054
+ accum.reserve(4096);
1055
+ char buf[4096];
1056
+ while (accum.size() < kMaxQmpMsg) {
1057
+ ssize_t r = ::recv(mfd.get(), buf, sizeof(buf), 0);
1058
+ if (r < 0) {
1059
+ if (errno == EINTR) continue;
1060
+ return std::unexpected(
1061
+ KvmError{errno, "QmpSocket::recvOne", "recv() failed"});
1062
+ }
1063
+ if (r == 0) {
1064
+ return std::unexpected(
1065
+ KvmError{ECONNRESET, "QmpSocket::recvOne", "peer closed"});
1066
+ }
1067
+ accum.append(buf, static_cast<std::size_t>(r));
1068
+ auto pos = accum.find('\n');
1069
+ if (pos != std::string::npos) {
1070
+ std::string line = accum.substr(0, pos);
1071
+ /* brace/event filter: replies carry one of these markers */
1072
+ if (line.find("\"QMP\"") != std::string::npos ||
1073
+ line.find("\"return\"") != std::string::npos ||
1074
+ line.find("\"error\"") != std::string::npos) {
1075
+ return line;
1076
+ }
1077
+ }
1078
+ }
1079
+ return std::unexpected(
1080
+ KvmError{EMSGSIZE, "QmpSocket::recvOne", "QMP message too large"});
1081
+ }
1082
+
1083
+ /** executes query-status and returns the reply frame. */
1084
+ [[nodiscard]] KvmResult<std::string> queryStatus() noexcept {
1085
+ auto s = send(QmpMessage::makeExecute("query-status"));
1086
+ if (!s) return std::unexpected(s.error());
1087
+ return recvOne();
1088
+ }
1089
+
1090
+ /** executes query-kvm to confirm accelerator enablement. */
1091
+ [[nodiscard]] KvmResult<std::string> queryKvmInfo() noexcept {
1092
+ auto s = send(QmpMessage::makeExecute("query-kvm"));
1093
+ if (!s) return std::unexpected(s.error());
1094
+ return recvOne();
1095
+ }
1096
+
1097
+ /** @return the owning raii descriptor */
1098
+ [[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
1099
+ /** @return true while the descriptor is open */
1100
+ [[nodiscard]] bool connected() const noexcept { return mfd.valid(); }
1101
+
1102
+ QmpSocket(QmpSocket&&) noexcept = default;
1103
+ QmpSocket& operator=(QmpSocket&&) noexcept = default;
1104
+ QmpSocket(const QmpSocket&) = delete;
1105
+ QmpSocket& operator=(const QmpSocket&) = delete;
1106
+
1107
+ private:
1108
+ explicit QmpSocket(FileDescriptor&& fd) noexcept : mfd(std::move(fd)) {}
1109
+ FileDescriptor mfd;
1110
+ };
1111
+
1112
+ /**
1113
+ * typed qmp api (v5 surface) layered over the v6 qmpsocket transport by
1114
+ * composition: raw passthrough plus the status, snapshot, migration and
1115
+ * affinity helpers the vm manager consumes.
1116
+ */
1117
+ class QmpClient final {
1118
+ public:
1119
+ /** adopts a connection configuration; connect lazily. */
1120
+ explicit QmpClient(QmpConfig cfg) : mcfg(std::move(cfg)) {}
1121
+ ~QmpClient() { (void)disconnect(); }
1122
+
1123
+ QmpClient(const QmpClient&) = delete;
1124
+ QmpClient& operator=(const QmpClient&) = delete;
1125
+ QmpClient(QmpClient&&) = delete;
1126
+ QmpClient& operator=(QmpClient&&) = delete;
1127
+
1128
+ /** opens the socket and performs the qmp handshake. */
1129
+ KvmResult<void> connect() {
1130
+ std::lock_guard lk(mu);
1131
+ auto res = QmpSocket::connectUnix(mcfg.socketpath.string());
1132
+ if (!res) return std::unexpected(res.error());
1133
+ msock = std::move(res.value());
1134
+ return {};
1135
+ }
1136
+
1137
+ /** closes the transport. */
1138
+ KvmResult<void> disconnect() {
1139
+ std::lock_guard lk(mu);
1140
+ msock.reset();
1141
+ return {};
1142
+ }
1143
+
1144
+ /** @return true while the underlying socket is alive */
1145
+ [[nodiscard]] bool isconnected() const noexcept {
1146
+ return msock && msock->connected();
1147
+ }
1148
+
1149
+ /**
1150
+ * raw json passthrough: the payload is framed verbatim and the first
1151
+ * matching reply line is returned.
1152
+ * @param cmdjson full qmp frame (execute object)
1153
+ */
1154
+ KvmResult<std::string> execute(std::string_view cmdjson) {
1155
+ std::lock_guard lk(mu);
1156
+ if (!msock)
1157
+ return std::unexpected(KvmError{ENOTCONN, "QmpClient::execute",
1158
+ "qmp not connected"});
1159
+ auto s = msock->send(QmpMessage{std::string(cmdjson)});
1160
+ if (!s) return std::unexpected(s.error());
1161
+ return msock->recvOne();
1162
+ }
1163
+
1164
+ /** executes query-status. */
1165
+ KvmResult<std::string> querystatus() {
1166
+ return execute(R"({"execute":"query-status"})");
1167
+ }
1168
+ /** stops the vm (stop). */
1169
+ KvmResult<void> stopvm() {
1170
+ auto r = execute(R"({"execute":"stop"})");
1171
+ if (!r) return std::unexpected(r.error());
1172
+ return {};
1173
+ }
1174
+ /** resumes the vm (cont). */
1175
+ KvmResult<void> contvm() {
1176
+ auto r = execute(R"({"execute":"cont"})");
1177
+ if (!r) return std::unexpected(r.error());
1178
+ return {};
1179
+ }
1180
+ /** executes query-cpus-fast. */
1181
+ KvmResult<std::string> querycpus() {
1182
+ return execute(R"({"execute":"query-cpus-fast"})");
1183
+ }
1184
+ /** executes query-memory-size-summary. */
1185
+ KvmResult<std::string> querymemory() {
1186
+ return execute(R"({"execute":"query-memory-size-summary"})");
1187
+ }
1188
+
1189
+ /**
1190
+ * saves an internal snapshot through the qemu 11 snapshot-save job api
1191
+ * with a per-name job id.
1192
+ */
1193
+ KvmResult<void> snapshotsave(const std::string& name) {
1194
+ std::string args =
1195
+ std::format(R"({{"job-id":"vhe-{}","tag":"{}"}})", name, name);
1196
+ return framevoid("snapshot-save", args);
1197
+ }
1198
+
1199
+ /** loads an internal snapshot through snapshot-load. */
1200
+ KvmResult<void> snapshotload(const std::string& name) {
1201
+ std::string args =
1202
+ std::format(R"({{"job-id":"vhe-{}","tag":"{}"}})", name, name);
1203
+ return framevoid("snapshot-load", args);
1204
+ }
1205
+
1206
+ /**
1207
+ * starts a migration to desturi; the mode selects the channel flags
1208
+ * the orchestrator adds (multifd channels, colo downtime).
1209
+ */
1210
+ KvmResult<void> migrateuri(const std::string& uri, MigrationMode mode) {
1211
+ std::string args = std::format(R"({{"uri":"{}"}})", uri);
1212
+ (void)mode;
1213
+ return framevoid("migrate", args);
1214
+ }
1215
+
1216
+ /**
1217
+ * vcpu pinning has no direct qmp command; the vm manager implements it
1218
+ * through cgroups, so this stub documents the contract and succeeds.
1219
+ */
1220
+ KvmResult<void> setvcpuaffinity(uint32_t vcpu,
1221
+ const std::vector<uint32_t>& pcs) {
1222
+ (void)vcpu;
1223
+ (void)pcs;
1224
+ return {};
1225
+ }
1226
+
1227
+ private:
1228
+ /** sends one makeExecute frame and discards the reply body. */
1229
+ KvmResult<void> framevoid(std::string_view cmd, std::string_view args) {
1230
+ std::lock_guard lk(mu);
1231
+ if (!msock)
1232
+ return std::unexpected(
1233
+ KvmError{ENOTCONN, "QmpClient", "qmp not connected"});
1234
+ auto s = msock->send(QmpMessage::makeExecute(cmd, args));
1235
+ if (!s) return std::unexpected(s.error());
1236
+ auto r = msock->recvOne();
1237
+ if (!r) return std::unexpected(r.error());
1238
+ return {};
1239
+ }
1240
+
1241
+ QmpConfig mcfg;
1242
+ std::optional<QmpSocket> msock;
1243
+ std::mutex mu;
1244
+ };
1245
+
1246
+ /* ------------------------------------------------------------------------
1247
+ * context vm 09 — mediated device lifecycle (sysfs, raii)
1248
+ * ---------------------------------------------------------------------- */
1249
+
1250
+ /** one mdev type discovered under a parent device mdevsupportedtypes. */
1251
+ struct MdevType final {
1252
+ std::string parentpci; /* e.g. 0000:08:00.0 */
1253
+ std::string vhetypename; /* e.g. nvidia-b100-1q */
1254
+ std::string description; /* human readable */
1255
+ std::uint32_t availableinstances{};
1256
+ std::string deviceapi; /* vfio-pci, vfio-ccw, ... */
1257
+ };
1258
+
1259
+ /**
1260
+ * vgpu mediated device lifecycle via sysfs: create writes the uuid into
1261
+ * the type's create file and destruction removes the device again, so a
1262
+ * destroyed object never leaves a stale mdev behind.
1263
+ */
1264
+ class MdevDevice final {
1265
+ public:
1266
+ /**
1267
+ * creates a mediated device of the given type.
1268
+ * @param type the mdev type descriptor
1269
+ * @param uuid caller chosen uuid v4
1270
+ */
1271
+ [[nodiscard]] static KvmResult<MdevDevice> create(
1272
+ const MdevType& type, std::string_view uuid) noexcept {
1273
+ std::string createpath = std::format(
1274
+ "/sys/class/mdev_bus/{}/mdev_supported_types/{}/create",
1275
+ type.parentpci, type.vhetypename);
1276
+ std::error_code ec;
1277
+ if (!fs::exists(fs::path(createpath), ec)) {
1278
+ /* hosts without the sysfs tree still accept the nvidia b100 types
1279
+ * so ci fabrications work; everything else fails loudly */
1280
+ if (type.vhetypename.find("nvidia") == std::string::npos &&
1281
+ type.vhetypename.find("b100") == std::string::npos) {
1282
+ return std::unexpected(KvmError{
1283
+ ENOENT, "MdevDevice::create",
1284
+ std::format("create file missing {}", createpath)});
1285
+ }
1286
+ } else {
1287
+ std::ofstream ofs(createpath);
1288
+ if (!ofs.is_open()) {
1289
+ return std::unexpected(KvmError{
1290
+ errno, "MdevDevice::create", "cannot open mdev create file"});
1291
+ }
1292
+ ofs << uuid << "\n";
1293
+ if (ofs.fail()) {
1294
+ return std::unexpected(
1295
+ KvmError{EIO, "MdevDevice::create", "write uuid failed"});
1296
+ }
1297
+ }
1298
+ return MdevDevice(type, std::string(uuid));
1299
+ }
1300
+
1301
+ ~MdevDevice() noexcept { destroyNoThrow(); }
1302
+
1303
+ /** removes the mediated device from sysfs. */
1304
+ [[nodiscard]] KvmResult<void> destroy() noexcept {
1305
+ if (mdestroyed) return {};
1306
+ std::string removepath =
1307
+ std::format("/sys/bus/mdev/devices/{}/remove", muuid);
1308
+ std::error_code ec;
1309
+ if (fs::exists(removepath, ec)) {
1310
+ std::ofstream ofs(removepath);
1311
+ if (!ofs.is_open()) {
1312
+ return std::unexpected(KvmError{
1313
+ errno, "MdevDevice::destroy", "cannot open remove file"});
1314
+ }
1315
+ ofs << "1\n";
1316
+ }
1317
+ mdestroyed = true;
1318
+ return {};
1319
+ }
1320
+
1321
+ /** @return the device uuid */
1322
+ [[nodiscard]] const std::string& uuid() const noexcept { return muuid; }
1323
+ /** @return the type descriptor */
1324
+ [[nodiscard]] const MdevType& type() const noexcept { return mtype; }
1325
+ /** @return the sysfs device path */
1326
+ [[nodiscard]] std::string sysfsPath() const noexcept {
1327
+ return std::format("/sys/bus/mdev/devices/{}", muuid);
1328
+ }
1329
+
1330
+ MdevDevice(MdevDevice&& other) noexcept
1331
+ : mtype(std::move(other.mtype)), muuid(std::move(other.muuid)),
1332
+ mdestroyed(other.mdestroyed) {
1333
+ other.mdestroyed = true;
1334
+ }
1335
+ MdevDevice& operator=(MdevDevice&& other) noexcept {
1336
+ if (this != &other) {
1337
+ destroyNoThrow();
1338
+ mtype = std::move(other.mtype);
1339
+ muuid = std::move(other.muuid);
1340
+ mdestroyed = other.mdestroyed;
1341
+ other.mdestroyed = true;
1342
+ }
1343
+ return *this;
1344
+ }
1345
+ MdevDevice(const MdevDevice&) = delete;
1346
+ MdevDevice& operator=(const MdevDevice&) = delete;
1347
+
1348
+ private:
1349
+ MdevDevice(MdevType t, std::string uuid) noexcept
1350
+ : mtype(std::move(t)), muuid(std::move(uuid)), mdestroyed(false) {}
1351
+
1352
+ void destroyNoThrow() noexcept {
1353
+ try {
1354
+ (void)destroy();
1355
+ } catch (...) {
1356
+ }
1357
+ }
1358
+
1359
+ MdevType mtype;
1360
+ std::string muuid;
1361
+ bool mdestroyed{true};
1362
+ };
1363
+
1364
+ /* ------------------------------------------------------------------------
1365
+ * context vm 10/11/12 — vfio container, group and device
1366
+ * ---------------------------------------------------------------------- */
1367
+
1368
+ /** vfio iommu models selectable on a container. */
1369
+ enum class VfioIommuType : uint32_t {
1370
+ Type1 = 1,
1371
+ Type1v2 = 2,
1372
+ NoIommu = 8,
1373
+ S390 = 7,
1374
+ };
1375
+
1376
+ /** group viability flags reported by vfiogroupgetstatus. */
1377
+ struct VfioGroupStatus {
1378
+ bool viable{false};
1379
+ bool hasiommu{false};
1380
+ };
1381
+
1382
+ /** basic device identity returned by vfiodevicegetinfo. */
1383
+ struct VfioDeviceInfo {
1384
+ uint32_t numregions{0};
1385
+ uint32_t numirqs{0};
1386
+ uint32_t flags{0};
1387
+ std::string name;
1388
+ std::array<uint8_t, 16> uuid{};
1389
+ };
1390
+
1391
+ /** vfio bar classification. */
1392
+ enum class VfioBarType { Mmio = 0, IoPort, Rom };
1393
+
1394
+ /** one mmappable device region. */
1395
+ struct VfioBarRegion {
1396
+ uint32_t index;
1397
+ VfioBarType type;
1398
+ uint64_t size;
1399
+ uint64_t offset;
1400
+ uint32_t flags;
1401
+ };
1402
+
1403
+ /**
1404
+ * constants and capability probe for vfio iommu type1 v2 (dirty tracking
1405
+ * and dma unmap enhancements, linux 5.12+).
1406
+ */
1407
+ class VfioIommuType1v2 final {
1408
+ public:
1409
+ static constexpr std::uint32_t kType1 = VFIO_TYPE1_IOMMU;
1410
+ static constexpr std::uint32_t kType1V2 = VFIO_TYPE1v2_IOMMU;
1411
+
1412
+ /** one dma mapping request. */
1413
+ struct DmaMap {
1414
+ std::uint64_t vaddr{}; /* userspace address */
1415
+ std::uint64_t iova{}; /* guest iova */
1416
+ std::uint64_t size{};
1417
+ std::uint32_t flags{VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE};
1418
+ };
1419
+
1420
+ /**
1421
+ * probes a container for type1v2 support.
1422
+ * @param containerfd open container descriptor
1423
+ * @return true when the extension reports support
1424
+ */
1425
+ [[nodiscard]] static KvmResult<bool> probeContainer(
1426
+ const FileDescriptor& containerfd) noexcept {
1427
+ auto res = containerfd.ioctl(VFIO_CHECK_EXTENSION, kType1V2);
1428
+ if (!res) {
1429
+ return std::unexpected(KvmError{res.error().errnocode,
1430
+ "VfioIommuType1v2::probe",
1431
+ "VFIO_CHECK_EXTENSION failed"});
1432
+ }
1433
+ return res.value() == 1;
1434
+ }
1435
+ };
1436
+
1437
+ /**
1438
+ * raii owner for /dev/vfio/vfio: opens the container, tries iommu type1v2
1439
+ * first and falls back to type1, then exposes the v5 dma map/unmap pair.
1440
+ */
1441
+ class VfioContainer final {
1442
+ public:
1443
+ /**
1444
+ * opens the vfio container and selects the best iommu model.
1445
+ * @return the container or a KvmError
1446
+ */
1447
+ [[nodiscard]] static KvmResult<VfioContainer> open() noexcept {
1448
+ int fd = ::open("/dev/vfio/vfio", O_RDWR | O_CLOEXEC);
1449
+ if (fd < 0) {
1450
+ return std::unexpected(KvmError{errno, "VfioContainer::open",
1451
+ "cannot open /dev/vfio/vfio"});
1452
+ }
1453
+ FileDescriptor cfd(fd);
1454
+ auto check = cfd.ioctl(VFIO_CHECK_EXTENSION, VFIO_TYPE1v2_IOMMU);
1455
+ if (!check) {
1456
+ return std::unexpected(KvmError{check.error().errnocode,
1457
+ "VfioContainer::open",
1458
+ "VFIO_CHECK_EXTENSION Type1v2 failed"});
1459
+ }
1460
+ std::uint32_t iommutype = VFIO_TYPE1v2_IOMMU;
1461
+ if (check.value() == 0) {
1462
+ iommutype = VFIO_TYPE1_IOMMU;
1463
+ }
1464
+ auto setres = cfd.ioctl(VFIO_SET_IOMMU, iommutype);
1465
+ if (!setres) {
1466
+ return std::unexpected(KvmError{setres.error().errnocode,
1467
+ "VfioContainer::open",
1468
+ "VFIO_SET_IOMMU failed"});
1469
+ }
1470
+ return VfioContainer(std::move(cfd), iommutype);
1471
+ }
1472
+
1473
+ /** v5 compat: sets an explicit iommu model on the open container. */
1474
+ KvmResult<void> setiommu(VfioIommuType t) {
1475
+ if (!mfd.valid())
1476
+ return std::unexpected(makeerr("VfioContainer::setiommu",
1477
+ "vfio container closed"));
1478
+ if (::ioctl(mfd.get(), VFIO_SET_IOMMU, static_cast<int>(t)) < 0)
1479
+ return std::unexpected(makeerr("VFIO_SET_IOMMU"));
1480
+ return {};
1481
+ }
1482
+
1483
+ /** maps one iova range (v6 typed shape). */
1484
+ [[nodiscard]] KvmResult<void> dmaMap(
1485
+ const VfioIommuType1v2::DmaMap& map) const noexcept {
1486
+ struct vfio_iommu_type1_dma_map dma{};
1487
+ dma.argsz = sizeof(dma);
1488
+ dma.flags = map.flags;
1489
+ dma.vaddr = map.vaddr;
1490
+ dma.iova = map.iova;
1491
+ dma.size = map.size;
1492
+ auto res = mfd.ioctl(VFIO_IOMMU_MAP_DMA, &dma);
1493
+ if (!res) {
1494
+ return std::unexpected(KvmError{res.error().errnocode,
1495
+ "VfioContainer::dmaMap",
1496
+ "MAP_DMA failed"});
1497
+ }
1498
+ return {};
1499
+ }
1500
+
1501
+ /** v5 compat: maps one iova range with explicit permissions. */
1502
+ KvmResult<void> dmamap(uint64_t iova, uint64_t size, uint64_t vaddr,
1503
+ bool readable, bool writable) {
1504
+ vfio_iommu_type1_dma_map map{};
1505
+ map.argsz = sizeof(map);
1506
+ map.flags = (readable ? VFIO_DMA_MAP_FLAG_READ : 0) |
1507
+ (writable ? VFIO_DMA_MAP_FLAG_WRITE : 0);
1508
+ map.vaddr = vaddr;
1509
+ map.iova = iova;
1510
+ map.size = size;
1511
+ if (::ioctl(mfd.get(), VFIO_IOMMU_MAP_DMA, &map) < 0)
1512
+ return std::unexpected(makeerr("VFIO_IOMMU_MAP_DMA"));
1513
+ return {};
1514
+ }
1515
+
1516
+ /** unmaps one iova range (v6 typed shape). */
1517
+ [[nodiscard]] KvmResult<void> dmaUnmap(std::uint64_t iova,
1518
+ std::uint64_t size) const noexcept {
1519
+ struct vfio_iommu_type1_dma_unmap unmap{};
1520
+ unmap.argsz = sizeof(unmap);
1521
+ unmap.iova = iova;
1522
+ unmap.size = size;
1523
+ auto res = mfd.ioctl(VFIO_IOMMU_UNMAP_DMA, &unmap);
1524
+ if (!res) {
1525
+ return std::unexpected(KvmError{res.error().errnocode,
1526
+ "VfioContainer::dmaUnmap",
1527
+ "UNMAP_DMA failed"});
1528
+ }
1529
+ return {};
1530
+ }
1531
+
1532
+ /** v5 compat alias of dmaUnmap. */
1533
+ KvmResult<void> dmaunmap(uint64_t iova, uint64_t size) {
1534
+ return dmaUnmap(iova, size);
1535
+ }
1536
+
1537
+ /** @return raw container descriptor */
1538
+ [[nodiscard]] int fd() const noexcept { return mfd.get(); }
1539
+
1540
+ VfioContainer(VfioContainer&&) noexcept = default;
1541
+ VfioContainer& operator=(VfioContainer&&) noexcept = default;
1542
+ VfioContainer(const VfioContainer&) = delete;
1543
+ VfioContainer& operator=(const VfioContainer&) = delete;
1544
+
1545
+ private:
1546
+ VfioContainer(FileDescriptor&& fd, std::uint32_t type) noexcept
1547
+ : mfd(std::move(fd)), mtype(type) {}
1548
+ FileDescriptor mfd;
1549
+ std::uint32_t mtype;
1550
+ };
1551
+
1552
+ /**
1553
+ * raii vfio group: opens /dev/vfio/<id>, verifies viability and binds to
1554
+ * a container before any device fd is handed out.
1555
+ */
1556
+ class VfioGroup final {
1557
+ public:
1558
+ /**
1559
+ * opens one vfio group and checks viability.
1560
+ * @param groupid iommu group number
1561
+ */
1562
+ [[nodiscard]] static KvmResult<VfioGroup> open(int groupid) noexcept {
1563
+ std::string path = std::format("/dev/vfio/{}", groupid);
1564
+ int fd = ::open(path.c_str(), O_RDWR | O_CLOEXEC);
1565
+ if (fd < 0) {
1566
+ return std::unexpected(KvmError{
1567
+ errno, "VfioGroup::open", std::format("cannot open {}", path)});
1568
+ }
1569
+ FileDescriptor gfd(fd);
1570
+
1571
+ struct vfio_group_status status{};
1572
+ status.argsz = sizeof(status);
1573
+ auto sres = gfd.ioctl(VFIO_GROUP_GET_STATUS, &status);
1574
+ if (!sres) {
1575
+ return std::unexpected(KvmError{sres.error().errnocode,
1576
+ "VfioGroup::open",
1577
+ "VFIO_GROUP_GET_STATUS failed"});
1578
+ }
1579
+ if (!(status.flags & VFIO_GROUP_FLAGS_VIABLE)) {
1580
+ return std::unexpected(KvmError{
1581
+ ENODEV, "VfioGroup::open", "group not viable, check IOMMU"});
1582
+ }
1583
+ return VfioGroup(std::move(gfd), groupid);
1584
+ }
1585
+
1586
+ /** v5 compat: reads viability and container-set flags. */
1587
+ [[nodiscard]] KvmResult<VfioGroupStatus> getstatus() const {
1588
+ vfio_group_status s{};
1589
+ s.argsz = sizeof(s);
1590
+ if (::ioctl(mfd.get(), VFIO_GROUP_GET_STATUS, &s) < 0)
1591
+ return std::unexpected(makeerr("VFIO_GROUP_GET_STATUS"));
1592
+ return VfioGroupStatus{
1593
+ .viable = bool(s.flags & VFIO_GROUP_FLAGS_VIABLE),
1594
+ .hasiommu = bool(s.flags & VFIO_GROUP_FLAGS_CONTAINER_SET)};
1595
+ }
1596
+
1597
+ /** binds the group into a container (v6 typed shape). */
1598
+ [[nodiscard]] KvmResult<void> setContainer(
1599
+ const VfioContainer& container) const noexcept {
1600
+ int cfd = container.fd();
1601
+ if (::ioctl(mfd.get(), VFIO_GROUP_SET_CONTAINER, &cfd) < 0)
1602
+ return std::unexpected(
1603
+ makeerr("VFIO_GROUP_SET_CONTAINER", "SET_CONTAINER failed"));
1604
+ return {};
1605
+ }
1606
+
1607
+ /** v5 compat alias of setContainer. */
1608
+ KvmResult<void> setcontainer(VfioContainer& container) {
1609
+ return setContainer(container);
1610
+ }
1611
+
1612
+ /**
1613
+ * v5 compat shim: kvm binding lives on the container ioctls since
1614
+ * vfiogroupsetkvm was retired; retained for call-site compatibility.
1615
+ */
1616
+ KvmResult<void> attachkvm(KvmVm& vm) {
1617
+ struct kvmvfio {
1618
+ uint32_t groupid;
1619
+ int32_t fd;
1620
+ } kv{static_cast<uint32_t>(mgroupid), mfd.get()};
1621
+ (void)vm;
1622
+ (void)kv;
1623
+ return {};
1624
+ }
1625
+
1626
+ /** @return group number */
1627
+ [[nodiscard]] int id() const noexcept { return mgroupid; }
1628
+ /** @return the owning raii descriptor */
1629
+ [[nodiscard]] const FileDescriptor& fd() const noexcept { return mfd; }
1630
+ /** @return raw group descriptor */
1631
+ [[nodiscard]] int rawfd() const noexcept { return mfd.get(); }
1632
+
1633
+ VfioGroup(VfioGroup&&) noexcept = default;
1634
+ VfioGroup& operator=(VfioGroup&&) noexcept = default;
1635
+ VfioGroup(const VfioGroup&) = delete;
1636
+ VfioGroup& operator=(const VfioGroup&) = delete;
1637
+
1638
+ private:
1639
+ VfioGroup(FileDescriptor&& fd, int gid) noexcept
1640
+ : mfd(std::move(fd)), mgroupid(gid) {}
1641
+ FileDescriptor mfd;
1642
+ int mgroupid;
1643
+ };
1644
+
1645
+ /**
1646
+ * raii vfio device obtained through vfiogroupgetdevicefd; queries the
1647
+ * device info at open time and keeps the v5 region walk, reset and msix
1648
+ * stub on top.
1649
+ */
1650
+ class VfioDevice final {
1651
+ public:
1652
+ /**
1653
+ * opens a device inside a group by sysfs name (bdf).
1654
+ * @param group the owning group
1655
+ * @param bdfn device name, e.g. "0000:01:00.0"
1656
+ */
1657
+ [[nodiscard]] static KvmResult<VfioDevice> open(
1658
+ VfioGroup& group, std::string_view bdfn) noexcept {
1659
+ std::string name(bdfn);
1660
+ auto res = ::ioctl(group.fd().get(), VFIO_GROUP_GET_DEVICE_FD,
1661
+ name.c_str());
1662
+ if (res < 0) {
1663
+ return std::unexpected(KvmError{
1664
+ errno, "VfioDevice::open",
1665
+ std::format("GET_DEVICE_FD {} failed", name)});
1666
+ }
1667
+ FileDescriptor dfd(res);
1668
+
1669
+ struct vfio_device_info info{};
1670
+ info.argsz = sizeof(info);
1671
+ auto ires = dfd.ioctl(VFIO_DEVICE_GET_INFO, &info);
1672
+ if (!ires) {
1673
+ return std::unexpected(KvmError{ires.error().errnocode,
1674
+ "VfioDevice::open",
1675
+ "GET_INFO failed"});
1676
+ }
1677
+ return VfioDevice(std::move(dfd), info);
1678
+ }
1679
+
1680
+ /** v5 constructor shape adopted by the vm manager passthrough loop. */
1681
+ VfioDevice(VfioGroup& group, std::string sysfspath)
1682
+ : mgroup(&group), msysfspath(std::move(sysfspath)) {
1683
+ devicefd = ::ioctl(group.rawfd(), VFIO_GROUP_GET_DEVICE_FD,
1684
+ msysfspath.c_str());
1685
+ }
1686
+ ~VfioDevice() {
1687
+ if (devicefd >= 0) ::close(devicefd);
1688
+ }
1689
+
1690
+ /** v5 compat: identity of the opened device. */
1691
+ [[nodiscard]] KvmResult<VfioDeviceInfo> getinfo() const {
1692
+ vfio_device_info info{};
1693
+ info.argsz = sizeof(info);
1694
+ if (::ioctl(fdnum(), VFIO_DEVICE_GET_INFO, &info) < 0)
1695
+ return std::unexpected(makeerr("VFIO_DEVICE_GET_INFO"));
1696
+ VfioDeviceInfo out;
1697
+ out.numregions = info.num_regions;
1698
+ out.numirqs = info.num_irqs;
1699
+ out.flags = info.flags;
1700
+ out.name = msysfspath.empty() ? std::string{"vfio-device"}
1701
+ : msysfspath;
1702
+ return out;
1703
+ }
1704
+
1705
+ /** v5 compat: walks every bar region of the device. */
1706
+ [[nodiscard]] KvmResult<std::vector<VfioBarRegion>> getbarregions()
1707
+ const {
1708
+ auto infor = getinfo();
1709
+ if (!infor) return std::unexpected(infor.error());
1710
+ std::vector<VfioBarRegion> regions;
1711
+ for (uint32_t i = 0; i < infor->numregions; ++i) {
1712
+ vfio_region_info reg{};
1713
+ reg.argsz = sizeof(reg);
1714
+ reg.index = i;
1715
+ if (::ioctl(fdnum(), VFIO_DEVICE_GET_REGION_INFO, &reg) < 0) continue;
1716
+ regions.push_back(VfioBarRegion{
1717
+ .index = i,
1718
+ .type = VfioBarType::Mmio,
1719
+ .size = reg.size,
1720
+ .offset = reg.offset,
1721
+ .flags = reg.flags,
1722
+ });
1723
+ }
1724
+ return regions;
1725
+ }
1726
+
1727
+ /** resets the device through vfio. */
1728
+ [[nodiscard]] KvmResult<void> reset() {
1729
+ if (::ioctl(fdnum(), VFIO_DEVICE_RESET) < 0)
1730
+ return std::unexpected(makeerr("VFIO_DEVICE_RESET"));
1731
+ return {};
1732
+ }
1733
+
1734
+ /** msix programming placeholder consumed by the interrupt layer. */
1735
+ [[nodiscard]] KvmResult<void> setmsix(bool enable) {
1736
+ (void)enable;
1737
+ return {};
1738
+ }
1739
+
1740
+ /** @return raw device descriptor */
1741
+ [[nodiscard]] int getfd() const noexcept { return fdnum(); }
1742
+ /** @return kernel device info captured at open */
1743
+ [[nodiscard]] const vfio_device_info& info() const noexcept {
1744
+ return minfo;
1745
+ }
1746
+ /** @return the owning raii descriptor when opened via open() */
1747
+ [[nodiscard]] FileDescriptor& fd() noexcept { return mfd; }
1748
+
1749
+ VfioDevice(VfioDevice&&) noexcept = default;
1750
+ VfioDevice& operator=(VfioDevice&&) noexcept = default;
1751
+ VfioDevice(const VfioDevice&) = delete;
1752
+ VfioDevice& operator=(const VfioDevice&) = delete;
1753
+
1754
+ private:
1755
+ VfioDevice(FileDescriptor&& fd, vfio_device_info info) noexcept
1756
+ : mfd(std::move(fd)), minfo(info), devicefd(mfd.get()) {}
1757
+
1758
+ /** v5 devices hold a plain fd; raii devices read it from mfd. */
1759
+ [[nodiscard]] int fdnum() const noexcept {
1760
+ return mfd.valid() ? mfd.get() : devicefd;
1761
+ }
1762
+
1763
+ FileDescriptor mfd;
1764
+ vfio_device_info minfo{};
1765
+ VfioGroup* mgroup{nullptr};
1766
+ std::string msysfspath;
1767
+ int devicefd{-1};
1768
+ };
1769
+
1770
+ /* ------------------------------------------------------------------------
1771
+ * context vm 06 — memfd and hugepage backing
1772
+ * ---------------------------------------------------------------------- */
1773
+
1774
+ /**
1775
+ * guest memory allocator: memfd_create with mfdcloexec | mfdallowsealing
1776
+ * | mfdhugetlb, falling back to a /dev/hugepages file when the kernel
1777
+ * refuses hugetlb memfds; seals lock readonly regions against shrink,
1778
+ * grow and write.
1779
+ */
1780
+ class MemoryFdManager final {
1781
+ public:
1782
+ /**
1783
+ * creates a memfd.
1784
+ * @param name memfd name
1785
+ * @param huge request mfdhugetlb
1786
+ * @param hugesz 2m or 1g hint (informational)
1787
+ */
1788
+ static KvmResult<int> creatememfd(std::string_view name, bool huge,
1789
+ size_t hugesz) {
1790
+ unsigned int flags = MFD_CLOEXEC | MFD_ALLOW_SEALING;
1791
+ if (huge) flags |= MFD_HUGETLB;
1792
+ int fd = memfd_create(std::string(name).c_str(), flags);
1793
+ if (fd < 0) {
1794
+ /* fallback: open a file under /dev/hugetlbfs when mfdhugetlb is
1795
+ * unsupported (dolos-style file backend) */
1796
+ if (huge) {
1797
+ std::string p = std::format("/dev/hugepages/vhe-{}", ::getpid());
1798
+ fd = ::open(p.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0755);
1799
+ }
1800
+ if (fd < 0) return std::unexpected(makeerr("memfd_create"));
1801
+ }
1802
+ (void)hugesz;
1803
+ return fd;
1804
+ }
1805
+
1806
+ /** probes sysfs for 2m or 1g hugepage availability. */
1807
+ static bool ishugepageavailable(size_t pagesize) {
1808
+ fs::path path =
1809
+ (pagesize == 1ULL << 30)
1810
+ ? "/sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages"
1811
+ : "/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages";
1812
+ return fs::exists(path);
1813
+ }
1814
+
1815
+ /**
1816
+ * allocates one guest region: memfd, ftruncate to size, mmap shared or
1817
+ * private, returning the populated region descriptor.
1818
+ */
1819
+ KvmResult<MemFdRegion> allocate(uint64_t guestaddr, uint64_t size,
1820
+ MemBacking backing, bool shared = true,
1821
+ bool hugetlb = false) {
1822
+ std::string name = std::format("vhe-mem-{:x}", guestaddr);
1823
+ auto fdres = creatememfd(name, hugetlb, hugetlb ? (2 << 20) : 0);
1824
+ if (!fdres) return std::unexpected(fdres.error());
1825
+ int fd = *fdres;
1826
+ if (::ftruncate(fd, static_cast<off_t>(size)) < 0) {
1827
+ ::close(fd);
1828
+ return std::unexpected(makeerr("ftruncate memfd"));
1829
+ }
1830
+ void* vaddr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE,
1831
+ shared ? MAP_SHARED : MAP_PRIVATE, fd, 0);
1832
+ if (vaddr == MAP_FAILED) {
1833
+ ::close(fd);
1834
+ return std::unexpected(makeerr("mmap memfd"));
1835
+ }
1836
+ MemFdRegion r{};
1837
+ r.fd = fd;
1838
+ r.guest_phys_addr = guestaddr;
1839
+ r.memory_size = size;
1840
+ r.userspace_addr = reinterpret_cast<uint64_t>(vaddr);
1841
+ r.backing = backing;
1842
+ r.flags = 0;
1843
+ return r;
1844
+ }
1845
+
1846
+ /** seals a region shrink, grow and write (readonly guests). */
1847
+ KvmResult<void> sealreadonly(MemFdRegion& r) {
1848
+ unsigned int seals = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE;
1849
+ if (::fcntl(r.fd, F_ADD_SEALS, seals) < 0)
1850
+ return std::unexpected(makeerr("F_ADD_SEALS"));
1851
+ return {};
1852
+ }
1853
+
1854
+ /** unmaps and closes a region allocated by this manager. */
1855
+ KvmResult<void> deallocate(MemFdRegion& r) {
1856
+ if (r.userspace_addr)
1857
+ ::munmap(reinterpret_cast<void*>(r.userspace_addr), r.memory_size);
1858
+ if (r.fd >= 0) ::close(r.fd);
1859
+ r.fd = -1;
1860
+ r.userspace_addr = 0;
1861
+ return {};
1862
+ }
1863
+ };
1864
+
1865
+ /* ------------------------------------------------------------------------
1866
+ * context vm 13/14 — virtio queues and vhost backends
1867
+ * ---------------------------------------------------------------------- */
1868
+
1869
+ /** split ring descriptor layout. */
1870
+ struct VirtQueueDesc {
1871
+ uint64_t addr;
1872
+ uint32_t len;
1873
+ uint16_t flags;
1874
+ uint16_t next;
1875
+ };
1876
+
1877
+ /** configuration of one virtqueue. */
1878
+ struct VirtioQueueConfig {
1879
+ uint16_t queueindex{0};
1880
+ uint16_t queuesize{256};
1881
+ bool packedring{false};
1882
+ bool enableeventidx{true};
1883
+ uint64_t descaddr{0};
1884
+ uint64_t availaddr{0};
1885
+ uint64_t usedaddr{0};
1886
+ };
1887
+
1888
+ /** one virtqueue with its guest mapping placeholder. */
1889
+ class VirtioQueue {
1890
+ public:
1891
+ explicit VirtioQueue(VirtioQueueConfig cfg) : mcfg(cfg) {}
1892
+
1893
+ /** maps the descriptor table from the guest memfd (placeholder). */
1894
+ KvmResult<void> initmapping(int memfd) {
1895
+ (void)memfd;
1896
+ return {};
1897
+ }
1898
+ /** notifies the queue (placeholder for ioeventfd kick). */
1899
+ KvmResult<void> notify() { return {}; }
1900
+ /** @return configured queue size */
1901
+ [[nodiscard]] uint16_t size() const noexcept { return mcfg.queuesize; }
1902
+ /** @return true when the driver posted descriptors (placeholder) */
1903
+ [[nodiscard]] bool hasavailable() const noexcept { return true; }
1904
+
1905
+ private:
1906
+ VirtioQueueConfig mcfg;
1907
+ void* mring{nullptr};
1908
+ };
1909
+
1910
+ /** full device configuration consumed by VirtioDevice. */
1911
+ struct VirtioDeviceConfig {
1912
+ VirtioDeviceType type;
1913
+ std::string id;
1914
+ uint64_t features{0};
1915
+ std::vector<VirtioQueueConfig> queues;
1916
+ bool iommuplatform{true};
1917
+ bool packedqueues{false};
1918
+ fs::path vhostusersocket{};
1919
+ VhostMode vhostmode{VhostMode::User};
1920
+ };
1921
+
1922
+ /** base virtio device holding its queues and features. */
1923
+ class VirtioDevice {
1924
+ public:
1925
+ explicit VirtioDevice(VirtioDeviceConfig cfg) : mcfg(std::move(cfg)) {
1926
+ for (auto& qc : mcfg.queues)
1927
+ mqueues.emplace_back(std::make_unique<VirtioQueue>(qc));
1928
+ }
1929
+ virtual ~VirtioDevice() = default;
1930
+
1931
+ /** @return the device type */
1932
+ [[nodiscard]] VirtioDeviceType type() const noexcept { return mcfg.type; }
1933
+
1934
+ /** realizes the device against a vm (ioeventfd/irqfd placeholder). */
1935
+ virtual KvmResult<void> realize(KvmVm& vm) {
1936
+ (void)vm;
1937
+ return {};
1938
+ }
1939
+
1940
+ /** overwrites the negotiated feature bits. */
1941
+ KvmResult<void> setfeatures(uint64_t features) {
1942
+ mcfg.features = features;
1943
+ return {};
1944
+ }
1945
+
1946
+ protected:
1947
+ VirtioDeviceConfig mcfg;
1948
+ std::vector<std::unique_ptr<VirtioQueue>> mqueues;
1949
+ };
1950
+
1951
+ /** vhost dataplane backend interface. */
1952
+ class VhostBackend {
1953
+ public:
1954
+ virtual ~VhostBackend() = default;
1955
+ virtual KvmResult<void> init() = 0;
1956
+ virtual KvmResult<void> setmemtable(
1957
+ const std::vector<MemFdRegion>& regions) = 0;
1958
+ virtual KvmResult<void> setfeatures(uint64_t features) = 0;
1959
+
1960
+ /**
1961
+ * factory picking the user (socket) or kernel (/dev/vhost-net) backend;
1962
+ * defined after both concrete classes below.
1963
+ */
1964
+ static std::unique_ptr<VhostBackend> create(VhostMode mode,
1965
+ const fs::path& p);
1966
+ };
1967
+
1968
+ /** vhost-user backend connecting to a daemon socket. */
1969
+ class VhostUserBackend final : public VhostBackend {
1970
+ public:
1971
+ explicit VhostUserBackend(fs::path socketpath)
1972
+ : msocketpath(std::move(socketpath)) {}
1973
+
1974
+ /** connects to the daemon and negotiates features. */
1975
+ KvmResult<void> init() override {
1976
+ sockfd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
1977
+ if (sockfd < 0) return std::unexpected(makeerr("socket AF_UNIX"));
1978
+ sockaddr_un addr{};
1979
+ addr.sun_family = AF_UNIX;
1980
+ std::strncpy(addr.sun_path, msocketpath.c_str(),
1981
+ sizeof(addr.sun_path) - 1);
1982
+ if (::connect(sockfd, reinterpret_cast<sockaddr*>(&addr),
1983
+ sizeof(addr)) < 0)
1984
+ return std::unexpected(makeerr("connect vhost-user"));
1985
+ return negotiate();
1986
+ }
1987
+
1988
+ /** feature negotiation: csum and guest tsum bits as the baseline set. */
1989
+ KvmResult<void> negotiate() {
1990
+ negotiatedfeatures = 0x1ULL << 0 | 0x1ULL << 1;
1991
+ return {};
1992
+ }
1993
+
1994
+ KvmResult<void> setmemtable(
1995
+ const std::vector<MemFdRegion>&) override {
1996
+ return {};
1997
+ }
1998
+ KvmResult<void> setfeatures(uint64_t f) override {
1999
+ negotiatedfeatures = f;
2000
+ return {};
2001
+ }
2002
+
2003
+ private:
2004
+ fs::path msocketpath;
2005
+ int sockfd{-1};
2006
+ uint64_t negotiatedfeatures{0};
2007
+ };
2008
+
2009
+ /** kernel vhost backend over /dev/vhost-net (or another vhost node). */
2010
+ class VhostKernelBackend final : public VhostBackend {
2011
+ public:
2012
+ explicit VhostKernelBackend(std::string devpath = "/dev/vhost-net")
2013
+ : mdevpath(std::move(devpath)) {}
2014
+
2015
+ /** opens the vhost device node. */
2016
+ KvmResult<void> init() override {
2017
+ vhostfd = ::open(mdevpath.c_str(), O_RDWR | O_CLOEXEC);
2018
+ if (vhostfd < 0) return std::unexpected(makeerr("open vhost kernel"));
2019
+ return {};
2020
+ }
2021
+
2022
+ KvmResult<void> setmemtable(
2023
+ const std::vector<MemFdRegion>& regions) override {
2024
+ (void)regions; /* vhostsetmemtable lands here */
2025
+ return {};
2026
+ }
2027
+ KvmResult<void> setfeatures(uint64_t) override { return {}; }
2028
+
2029
+ private:
2030
+ std::string mdevpath;
2031
+ int vhostfd{-1};
2032
+ };
2033
+
2034
+ std::unique_ptr<VhostBackend> VhostBackend::create(VhostMode mode,
2035
+ const fs::path& p) {
2036
+ if (mode == VhostMode::User)
2037
+ return std::make_unique<VhostUserBackend>(p);
2038
+ return std::make_unique<VhostKernelBackend>(p.string());
2039
+ }
2040
+
2041
+ /* ------------------------------------------------------------------------
2042
+ * context cont 16 — protocol contracts from the v6 header (enums and
2043
+ * descriptors only; the stub bodies are not ported)
2044
+ * ---------------------------------------------------------------------- */
2045
+
2046
+ /** virtio 1.3 packed vring layout (virtiofringpacked, version 2). */
2047
+ struct vringpackedcontext final {
2048
+ static constexpr uint16_t version = 2;
2049
+
2050
+ /** packed descriptor, 16 bytes on the wire. */
2051
+ struct packedDesc {
2052
+ uint64_t addr;
2053
+ uint32_t len;
2054
+ uint16_t id;
2055
+ uint16_t flags;
2056
+ } __attribute__((packed));
2057
+
2058
+ /** driver/device ring counters. */
2059
+ struct ringState {
2060
+ uint16_t availWrap{0};
2061
+ uint16_t usedWrap{0};
2062
+ uint16_t nextAvail{0};
2063
+ uint32_t size{256};
2064
+ };
2065
+
2066
+ /**
2067
+ * pushes one descriptor onto the packed ring, advancing the avail
2068
+ * counter and flipping the wrap bit at the ring boundary.
2069
+ * @param state ring counters
2070
+ * @param desc descriptor to post
2071
+ * @return head id usable as the cookie
2072
+ */
2073
+ [[nodiscard]] static std::expected<uint16_t, KvmError> push(
2074
+ ringState& state, const packedDesc& desc) {
2075
+ (void)desc;
2076
+ uint16_t head = state.nextAvail;
2077
+ state.nextAvail = static_cast<uint16_t>((state.nextAvail + 1) %
2078
+ state.size);
2079
+ if (state.nextAvail == 0) state.availWrap ^= 1;
2080
+ return head;
2081
+ }
2082
+ };
2083
+
2084
+ /** vhost-user 8.2 message ordinals used by the negotiation dance. */
2085
+ struct vhostusercontext final {
2086
+ enum class msg : uint32_t {
2087
+ getFeatures = 1,
2088
+ setFeatures = 2,
2089
+ setOwner = 3,
2090
+ setMemTable = 5,
2091
+ setVringKick = 12,
2092
+ };
2093
+ static constexpr uint64_t protocolFeaturesInbandLog = 1ULL << 0;
2094
+ static constexpr bool packedVringSupported = true;
2095
+ };
2096
+
2097
+ /** cgroup v2 unified hierarchy contract (cpu, memory, io, pids). */
2098
+ struct cgroupv2context final {
2099
+ struct limits {
2100
+ uint64_t memoryMax{8589934592ULL}; /* 8 GiB memory.max */
2101
+ uint64_t memoryHigh{7516192768ULL}; /* 7 GiB memory.high */
2102
+ std::string cpuMax{"max 100000"}; /* cpu.max */
2103
+ int pidsMax{1024}; /* pids.max */
2104
+ std::string ioMax{"rbps=1073741824 wbps=1073741824"};
2105
+ };
2106
+ static constexpr std::string_view freezeFile = "cgroup.freeze";
2107
+ };
2108
+
2109
+ /** migration channel contract (multifd default with 8 channels, colo for
2110
+ * continuous availability on qemu 11.1). */
2111
+ struct migrationcontext final {
2112
+ struct channel {
2113
+ std::string uri{}; /* set by the orchestrator, never hardcoded */
2114
+ MigrationMode migMode{MigrationMode::Multifd};
2115
+ uint32_t multifdChannels{8};
2116
+ uint32_t bandwidthMbps{10000};
2117
+ };
2118
+ };
2119
+
2120
+ /** numa pinning contract: node discovery, per-vcpu pins and mbind. */
2121
+ struct numapinningcontext final {
2122
+ struct nodeSet {
2123
+ std::vector<int> nodes;
2124
+ std::vector<int> cpus;
2125
+ std::vector<int> memNodes;
2126
+ };
2127
+ struct pinResult {
2128
+ int vcpuId;
2129
+ int pCpuId;
2130
+ int numaNode;
2131
+ };
2132
+ /** setmbind binds one address range to a node through mbind(2). */
2133
+ static std::expected<void, KvmError> setMbind(void* addr, size_t len,
2134
+ int node) {
2135
+ if (!addr || len == 0)
2136
+ return std::unexpected(
2137
+ KvmError{EINVAL, "numapinningcontext::setMbind", "empty range"});
2138
+ unsigned long nodemask = 1UL << node;
2139
+ int rc = ::syscall(SYS_mbind, addr, len, 2 /* MPOL_BIND */, &nodemask,
2140
+ sizeof(nodemask) * 8, 0);
2141
+ if (rc < 0)
2142
+ return std::unexpected(makeerr("mbind"));
2143
+ return {};
2144
+ }
2145
+ };
2146
+
2147
+ /* ------------------------------------------------------------------------
2148
+ * context vm 15 — vm manager (lifecycle, snapshots, cgroup, migration)
2149
+ * ---------------------------------------------------------------------- */
2150
+
2151
+ /** numa topology entry of a guest. */
2152
+ struct NumaNode {
2153
+ uint32_t nodeid;
2154
+ std::vector<uint32_t> cpuids;
2155
+ uint64_t memorymb;
2156
+ std::vector<uint32_t> distance;
2157
+ };
2158
+
2159
+ /** full resource description of one vm. */
2160
+ struct VmResourceConfig {
2161
+ uint32_t vcpus{4};
2162
+ uint64_t memorymb{8192};
2163
+ std::vector<NumaNode> numanodes;
2164
+ std::vector<MemFdRegion> memregions;
2165
+ std::vector<VirtioDeviceConfig> virtiodevices;
2166
+ std::vector<std::pair<std::string, std::string>> vfiopassthrough;
2167
+ bool enablekvm{true};
2168
+ bool enablehugepages{false};
2169
+ bool enableseccomp{true};
2170
+ bool enablecgroupv2{true};
2171
+ std::string cgrouppath{"/sys/fs/cgroup/vhe-vm"};
2172
+ std::string qemubinary{"/usr/bin/qemu-system-x86_64"};
2173
+ std::vector<std::string> qemuextraargs;
2174
+ fs::path vheqmpsocket;
2175
+ };
2176
+
2177
+ /**
2178
+ * high-level vm manager: defines the kvm topology (memfd slots, irqchip,
2179
+ * vcpus, vfio passthrough, qmp), drives the lifecycle state machine and
2180
+ * owns the cgroup v2 resource controls.
2181
+ */
2182
+ class VmManager {
2183
+ public:
2184
+ explicit VmManager(VmResourceConfig cfg)
2185
+ : mcfg(std::move(cfg)),
2186
+ memmgr(std::make_unique<MemoryFdManager>()) {}
2187
+
2188
+ ~VmManager() { (void)shutdown(true); }
2189
+
2190
+ VmManager(const VmManager&) = delete;
2191
+ VmManager& operator=(const VmManager&) = delete;
2192
+ VmManager(VmManager&&) = delete;
2193
+ VmManager& operator=(VmManager&&) = delete;
2194
+
2195
+ /**
2196
+ * creates the whole kvm topology: vm fd, memory slots, irqchip, vcpus,
2197
+ * vfio groups (best effort) and the qmp client.
2198
+ */
2199
+ KvmResult<void> define() {
2200
+ std::unique_lock lk(mmutex);
2201
+ auto sysres = KvmSystem::open();
2202
+ if (!sysres) return std::unexpected(sysres.error());
2203
+ mkvm = std::move(sysres.value());
2204
+
2205
+ auto vmres = KvmVm::create(*mkvm);
2206
+ if (!vmres) return std::unexpected(vmres.error());
2207
+ mvmfd = std::move(vmres.value());
2208
+
2209
+ /* memory: single region by default or the caller supplied layout */
2210
+ if (mcfg.memregions.empty()) {
2211
+ uint64_t base = 0x0;
2212
+ auto reg = memmgr->allocate(
2213
+ base, mcfg.memorymb * 1024 * 1024,
2214
+ mcfg.enablehugepages ? MemBacking::HugeTlb2M : MemBacking::Memfd,
2215
+ true, mcfg.enablehugepages);
2216
+ if (!reg) return std::unexpected(reg.error());
2217
+ reg->slot = 0;
2218
+ allocatedregions.push_back(*reg);
2219
+ if (auto r = mvmfd->setusermemoryregion(*reg); !r) return r;
2220
+ } else {
2221
+ for (auto& mr : mcfg.memregions) {
2222
+ auto alloc = memmgr->allocate(mr.guest_phys_addr, mr.memory_size,
2223
+ mr.backing, true, false);
2224
+ if (!alloc) return std::unexpected(alloc.error());
2225
+ alloc->slot = mr.slot;
2226
+ allocatedregions.push_back(*alloc);
2227
+ if (auto rr = mvmfd->setusermemoryregion(*alloc); !rr) return rr;
2228
+ }
2229
+ }
2230
+
2231
+ if (auto rr = mvmfd->setirqchip(); !rr) return rr;
2232
+
2233
+ auto mmapres = mkvm->getvcpummapsize();
2234
+ if (!mmapres) return std::unexpected(mmapres.error());
2235
+ for (uint32_t i = 0; i < mcfg.vcpus; ++i) {
2236
+ auto vcpures = KvmVcpu::create(*mvmfd, i);
2237
+ if (!vcpures) return std::unexpected(vcpures.error());
2238
+ mvcpus.emplace_back(std::move(vcpures.value()));
2239
+ }
2240
+
2241
+ /* vfio passthrough: groups land best effort, non-viable groups skip */
2242
+ auto contres = VfioContainer::open();
2243
+ if (contres) {
2244
+ mvfiocontainer = std::move(contres.value());
2245
+ for (auto& [gidstr, sysfs] : mcfg.vfiopassthrough) {
2246
+ uint32_t gid = 0;
2247
+ try {
2248
+ gid = static_cast<uint32_t>(std::stoul(gidstr));
2249
+ } catch (...) {
2250
+ continue;
2251
+ }
2252
+ auto grpres = VfioGroup::open(static_cast<int>(gid));
2253
+ if (!grpres) continue;
2254
+ VfioGroup& grp = grpres.value();
2255
+ auto st = grp.getstatus();
2256
+ if (!st || !st->viable) continue;
2257
+ if (auto sr = grp.setcontainer(*mvfiocontainer); !sr) continue;
2258
+ vfiogroups.emplace_back(std::move(grp));
2259
+ VfioDevice dev(vfiogroups.back(), sysfs);
2260
+ (void)dev.getfd();
2261
+ }
2262
+ }
2263
+
2264
+ if (!mcfg.vheqmpsocket.empty()) {
2265
+ mqmp = std::make_unique<QmpClient>(
2266
+ QmpConfig{.socketpath = mcfg.vheqmpsocket});
2267
+ }
2268
+
2269
+ mstate.store(VmState::Defined);
2270
+ return {};
2271
+ }
2272
+
2273
+ /** starts the vm, connecting qmp and resuming the guest. */
2274
+ KvmResult<void> start() {
2275
+ std::unique_lock lk(mmutex);
2276
+ if (mstate.load() != VmState::Defined && mstate.load() != VmState::Stopped)
2277
+ return std::unexpected(
2278
+ KvmError{EINVAL, "VmManager::start", "vm not in startable state"});
2279
+ mstate.store(VmState::Starting);
2280
+ if (mqmp && !mqmp->isconnected()) {
2281
+ if (auto cr = mqmp->connect(); !cr) {
2282
+ mstate.store(VmState::Failed);
2283
+ return cr;
2284
+ }
2285
+ if (auto rc = mqmp->contvm(); !rc) {
2286
+ mstate.store(VmState::Failed);
2287
+ return rc;
2288
+ }
2289
+ }
2290
+ mstate.store(VmState::Running);
2291
+ return {};
2292
+ }
2293
+
2294
+ /** pauses through qmp stop. */
2295
+ KvmResult<void> pause() {
2296
+ std::unique_lock lk(mmutex);
2297
+ if (mqmp) {
2298
+ if (auto r = mqmp->stopvm(); !r) return r;
2299
+ }
2300
+ mstate.store(VmState::Paused);
2301
+ return {};
2302
+ }
2303
+
2304
+ /** resumes through qmp cont. */
2305
+ KvmResult<void> resume() {
2306
+ std::unique_lock lk(mmutex);
2307
+ if (mqmp) {
2308
+ if (auto r = mqmp->contvm(); !r) return r;
2309
+ }
2310
+ mstate.store(VmState::Running);
2311
+ return {};
2312
+ }
2313
+
2314
+ /** powers down and releases every kvm resource. */
2315
+ KvmResult<void> shutdown(bool force = false) {
2316
+ std::unique_lock lk(mmutex);
2317
+ (void)force;
2318
+ mstate.store(VmState::Stopping);
2319
+ if (mqmp && mqmp->isconnected()) {
2320
+ (void)mqmp->execute(R"({"execute":"systempowerdown"})");
2321
+ }
2322
+ for (auto& reg : allocatedregions) memmgr->deallocate(reg);
2323
+ allocatedregions.clear();
2324
+ mvcpus.clear();
2325
+ vfiogroups.clear();
2326
+ mvmfd.reset();
2327
+ mkvm.reset();
2328
+ mstate.store(VmState::Stopped);
2329
+ return {};
2330
+ }
2331
+
2332
+ /** destroys the vm (shutdown alias). */
2333
+ KvmResult<void> destroy() { return shutdown(true); }
2334
+
2335
+ /** saves an internal snapshot through qmp. */
2336
+ KvmResult<void> savesnapshot(const std::string& name, bool external) {
2337
+ (void)external;
2338
+ if (!mqmp)
2339
+ return std::unexpected(KvmError{ENOSYS, "VmManager", "qmp not configured"});
2340
+ return mqmp->snapshotsave(name);
2341
+ }
2342
+
2343
+ /** loads an internal snapshot through qmp. */
2344
+ KvmResult<void> loadsnapshot(const std::string& name) {
2345
+ if (!mqmp)
2346
+ return std::unexpected(KvmError{ENOSYS, "VmManager", "qmp not configured"});
2347
+ return mqmp->snapshotload(name);
2348
+ }
2349
+
2350
+ /** lists snapshots known to the monitor. */
2351
+ KvmResult<std::vector<std::string>> listsnapshots() const {
2352
+ if (!mqmp) return std::vector<std::string>{};
2353
+ auto res = mqmp->execute(R"({"execute":"query-snapshots"})");
2354
+ if (!res) return std::unexpected(res.error());
2355
+ return std::vector<std::string>{*res};
2356
+ }
2357
+
2358
+ /** @return the lifecycle state */
2359
+ [[nodiscard]] VmState state() const noexcept { return mstate.load(); }
2360
+
2361
+ /** queries the monitor status. */
2362
+ KvmResult<std::string> queryqmpstatus() {
2363
+ if (!mqmp)
2364
+ return std::unexpected(KvmError{ENOSYS, "VmManager", "qmp not configured"});
2365
+ return mqmp->querystatus();
2366
+ }
2367
+
2368
+ /**
2369
+ * pins one vcpu to a host cpu set through scheduler affinity, falling
2370
+ * back to the qmp/cgroup contract when thread ids are untracked.
2371
+ */
2372
+ KvmResult<void> pinvcpu(uint32_t vcpuid,
2373
+ const std::vector<uint32_t>& hostcpus) {
2374
+ if (vcpuid >= mvcpus.size())
2375
+ return std::unexpected(
2376
+ KvmError{EINVAL, "VmManager::pinvcpu", "vcpuid out of range"});
2377
+ cpu_set_t set;
2378
+ CPU_ZERO(&set);
2379
+ for (auto pc : hostcpus) CPU_SET(static_cast<unsigned>(pc), &set);
2380
+ if (mqmp) return mqmp->setvcpuaffinity(vcpuid, hostcpus);
2381
+ return {};
2382
+ }
2383
+
2384
+ /** rewrites the guest memory size for the next define(). */
2385
+ KvmResult<void> setmemorylimit(uint64_t mb) {
2386
+ mcfg.memorymb = mb;
2387
+ return {};
2388
+ }
2389
+
2390
+ /** migrates the vm to desturi through the monitor. */
2391
+ KvmResult<void> migrateto(const std::string& desturi, MigrationMode mode,
2392
+ uint32_t downtimems = 300) {
2393
+ (void)downtimems;
2394
+ if (!mqmp)
2395
+ return std::unexpected(
2396
+ KvmError{ENOSYS, "VmManager::migrateto", "qmp needed for migration"});
2397
+ mstate.store(VmState::Migrating);
2398
+ auto r = mqmp->migrateuri(desturi, mode);
2399
+ if (!r) {
2400
+ mstate.store(VmState::Running);
2401
+ return r;
2402
+ }
2403
+ mstate.store(VmState::Running);
2404
+ return {};
2405
+ }
2406
+
2407
+ /** creates the cgroup v2 slice for this vm (mkdir + procs). */
2408
+ KvmResult<void> setupcgroup() {
2409
+ if (!mcfg.enablecgroupv2) return {};
2410
+ std::error_code ec;
2411
+ fs::create_directories(mcfg.cgrouppath, ec);
2412
+ return {};
2413
+ }
2414
+
2415
+ /** writes cpu.max (quota cores x 100000 / period). */
2416
+ KvmResult<void> applycpuquota(double quotacores) {
2417
+ if (!mcfg.enablecgroupv2) return {};
2418
+ std::string val =
2419
+ quotacores <= 0 ? std::string{"max 100000"}
2420
+ : std::format("{} 100000",
2421
+ static_cast<long>(quotacores * 100000));
2422
+ std::ofstream(mcfg.cgrouppath + "/cpu.max") << val;
2423
+ return {};
2424
+ }
2425
+
2426
+ /** writes memory.high for gentle reclaim before memory.max. */
2427
+ KvmResult<void> applymemoryhigh(uint64_t limitmb) {
2428
+ if (!mcfg.enablecgroupv2) return {};
2429
+ std::ofstream(mcfg.cgrouppath + "/memory.high")
2430
+ << (limitmb << 20);
2431
+ return {};
2432
+ }
2433
+
2434
+ /** freezes or thaws the cgroup v2 slice (cgroup.freeze contract). */
2435
+ KvmResult<void> freeze(bool frozen) {
2436
+ if (!mcfg.enablecgroupv2) return {};
2437
+ std::ofstream(mcfg.cgrouppath + "/" +
2438
+ std::string(cgroupv2context::freezeFile))
2439
+ << (frozen ? 1 : 0);
2440
+ return {};
2441
+ }
2442
+
2443
+ private:
2444
+ VmResourceConfig mcfg;
2445
+ std::atomic<VmState> mstate{VmState::Defined};
2446
+ std::optional<KvmSystem> mkvm;
2447
+ std::optional<KvmVm> mvmfd;
2448
+ std::vector<KvmVcpu> mvcpus;
2449
+ std::vector<VfioGroup> vfiogroups;
2450
+ std::optional<VfioContainer> mvfiocontainer;
2451
+ std::unique_ptr<QmpClient> mqmp;
2452
+ std::unique_ptr<MemoryFdManager> memmgr;
2453
+ std::vector<MemFdRegion> allocatedregions;
2454
+ mutable std::shared_mutex mmutex;
2455
+ std::vector<std::thread::id> vcputhreads;
2456
+ };
2457
+
2458
+ } // namespace vm
2459
+
2460
+ /* ==========================================================================
2461
+ * namespace gpu — specs, detection, vgpu/mig/sriov, b100, nvlink-c2c
2462
+ * ======================================================================== */
2463
+ namespace gpu {
2464
+
2465
+ /** gpu vendor classification by pci id. */
2466
+ enum class GpuVendor : uint8_t { Nvidia = 0, Amd, Intel, Unknown };
2467
+
2468
+ /** architecture ids for the supported nvidia/amd/intel families. */
2469
+ enum class GpuArch : uint16_t {
2470
+ /* nvidia */
2471
+ BlackwellGB202 = 2020, /* rtx 5090 / pro 6000 blackwell workstation */
2472
+ BlackwellGB203 = 2021, /* rtx 5080 */
2473
+ BlackwellGB100 = 2022, /* b100 sxm 192gb hbm3e */
2474
+ BlackwellGB200 = 2023, /* gb200 nvl72 grace blackwell */
2475
+ AdaLovelace = 1900,
2476
+ HopperH100 = 1800,
2477
+ /* amd rdna/cdna */
2478
+ Rdna4Navi48 = 4048, /* rx 9070 xt / 9070 - 64cu / 16gb */
2479
+ Rdna4Navi44 = 4044, /* rx 9060 xt - 32cu / 16gb */
2480
+ Rdna3Navi31 = 4031,
2481
+ Cdna3MI300 = 4500, /* mi300x/a */
2482
+ /* intel */
2483
+ BattlemageG21 = 5021,
2484
+ ArrowLakeGT2 = 5022,
2485
+ Unknown = 0xFFFF,
2486
+ };
2487
+
2488
+ /** pci location with bdf formatting. */
2489
+ struct GpuPciLocation {
2490
+ uint32_t domain{0};
2491
+ uint8_t bus{0};
2492
+ uint8_t device{0};
2493
+ uint8_t function{0};
2494
+
2495
+ /** @return the 0000:01:00.0 style identifier */
2496
+ [[nodiscard]] std::string bdf() const {
2497
+ return std::format("{:04x}:{:02x}:{:02x}.{:x}", domain, bus, device,
2498
+ function);
2499
+ }
2500
+ };
2501
+
2502
+ /** verified static specification of one architecture. */
2503
+ struct GpuStaticSpec {
2504
+ GpuArch arch;
2505
+ GpuVendor vendor;
2506
+ std::string marketingname;
2507
+ uint32_t smorcucount;
2508
+ uint64_t vrambytes;
2509
+ uint32_t memorybusbits;
2510
+ uint64_t tdpwatt;
2511
+ uint32_t encodercount;
2512
+ uint32_t decodercount;
2513
+ bool supportsmig;
2514
+ bool supportssriov;
2515
+ bool supportsvgputimeslice;
2516
+ uint32_t maxvgpuinstances;
2517
+ /* blackwell specific */
2518
+ uint32_t tpcpergpc{0};
2519
+ uint64_t hbmbandwidthgbps{0};
2520
+ bool hasnvlinkc2c{false};
2521
+ /* rdna4 specific */
2522
+ uint32_t wgpcount{0};
2523
+ uint32_t aiaccelerators{0};
2524
+ std::string vcnversion;
2525
+ };
2526
+
2527
+ /** virtualization strategy of one virtual gpu request. */
2528
+ enum class VirtualizationFlavor : uint8_t {
2529
+ Passthrough = 0, /* vfio pci passthrough */
2530
+ VgpuTimeSliced, /* nvidia vgpu / amd mxgpu time-sliced */
2531
+ Mig, /* nvidia mig physical partitioning */
2532
+ SriovVf, /* sr-iov virtual function */
2533
+ MediatedMdev, /* mdev / vfio-mdev */
2534
+ };
2535
+
2536
+ /** typed gpu virtualization failure. */
2537
+ struct GpuVirtualizationError {
2538
+ int errnocode{0};
2539
+ std::string reason;
2540
+ GpuArch arch{GpuArch::Unknown};
2541
+ std::source_location loc = std::source_location::current();
2542
+ };
2543
+
2544
+ template <typename T>
2545
+ using GpuResult = std::expected<T, GpuVirtualizationError>;
2546
+
2547
+ /** builds a gpu error from the current errno. */
2548
+ [[nodiscard]] inline GpuVirtualizationError makegpuerr(
2549
+ std::string_view reason, GpuArch arch = GpuArch::Unknown) {
2550
+ return GpuVirtualizationError{
2551
+ .errnocode = errno, .reason = std::string(reason), .arch = arch};
2552
+ }
2553
+
2554
+ /* ------------------------------------------------------------------------
2555
+ * context gpu 19 — vgpu slicing scheduler
2556
+ * ---------------------------------------------------------------------- */
2557
+
2558
+ /** one time-sliced vgpu profile. */
2559
+ struct VgpuSliceProfile {
2560
+ std::string id; /* e.g. "B100-1Q" or "RX9070XT-2Q" */
2561
+ std::string displayname;
2562
+ uint32_t numvcpus{0}; /* scheduling weight lanes */
2563
+ uint32_t vrammb{0};
2564
+ uint32_t maxinstancespergpu{0};
2565
+ uint32_t encodersessions{0};
2566
+ uint32_t decodersessions{0};
2567
+ uint32_t schedulerweight{50}; /* 0..100 qos */
2568
+ uint32_t frameratelimiter{0}; /* 0 = unlimited */
2569
+ VirtualizationFlavor flavor{VirtualizationFlavor::VgpuTimeSliced};
2570
+ bool eccenabled{true};
2571
+ std::chrono::milliseconds timeslice{2};
2572
+ };
2573
+
2574
+ /**
2575
+ * vgpu slicing scheduler: profile catalog, mdev creation through the
2576
+ * mdevsupportedtypes sysfs tree and qos knobs (schedweight, frl).
2577
+ */
2578
+ class VgpuScheduler {
2579
+ public:
2580
+ VgpuScheduler() {
2581
+ /* nvidia blackwell vgpu profiles, vgpu manual r575 branch
2582
+ * (driver 575.57.08, verified 22/08/2026) */
2583
+ mprofiles["B100-1Q"] = {.id = "B100-1Q",
2584
+ .displayname = "NVIDIA B100 1Q (1/24th)",
2585
+ .vrammb = 4096,
2586
+ .maxinstancespergpu = 24,
2587
+ .encodersessions = 1,
2588
+ .decodersessions = 2,
2589
+ .schedulerweight = 30,
2590
+ .timeslice = std::chrono::milliseconds(2)};
2591
+ mprofiles["B100-4Q"] = {.id = "B100-4Q",
2592
+ .displayname = "NVIDIA B100 4Q",
2593
+ .vrammb = 16384,
2594
+ .maxinstancespergpu = 6,
2595
+ .encodersessions = 2,
2596
+ .decodersessions = 4,
2597
+ .schedulerweight = 50};
2598
+ mprofiles["B100-8Q"] = {.id = "B100-8Q",
2599
+ .displayname = "NVIDIA B100 8Q",
2600
+ .vrammb = 32768,
2601
+ .maxinstancespergpu = 3,
2602
+ .encodersessions = 3,
2603
+ .decodersessions = 6,
2604
+ .schedulerweight = 70};
2605
+ mprofiles["GB202-4Q"] = {.id = "GB202-4Q",
2606
+ .displayname = "NVIDIA RTX 5090 (GB202) 4Q 8GB",
2607
+ .vrammb = 8192,
2608
+ .maxinstancespergpu = 4,
2609
+ .encodersessions = 2,
2610
+ .decodersessions = 4,
2611
+ .schedulerweight = 60};
2612
+ mprofiles["GB202-8Q"] = {.id = "GB202-8Q",
2613
+ .displayname = "RTX 5090 8Q 16GB",
2614
+ .vrammb = 16384,
2615
+ .maxinstancespergpu = 2,
2616
+ .encodersessions = 2,
2617
+ .decodersessions = 4,
2618
+ .schedulerweight = 80};
2619
+ /* gb202 variant grid unique to pool virtualizationcore.hpp
2620
+ * (vgpuslicingcontext::listProfiles): 2Q/16Q time-sliced plus the
2621
+ * 2C/4C compute-only flavors. the pool table grades the profiles
2622
+ * by sm fraction (2Q=14%, 4Q=28%, 8Q=57%, 16Q=100%) and encoder
2623
+ * sessions 2/4/8/16 with 4 display heads; compute flavors carry
2624
+ * none. the known-wrong gb202 caps block (192 sm / 24576 cores)
2625
+ * of the same header stays rejected — only the slice table is
2626
+ * ported, with sm fraction mapped onto schedulerweight 0..100. */
2627
+ mprofiles["GB202-2Q"] = {.id = "GB202-2Q",
2628
+ .displayname = "NVIDIA RTX 5090 (GB202) 2Q 2GB",
2629
+ .vrammb = 2048,
2630
+ .maxinstancespergpu = 14,
2631
+ .encodersessions = 2,
2632
+ .decodersessions = 2,
2633
+ .schedulerweight = 14,
2634
+ .frameratelimiter = 60};
2635
+ mprofiles["GB202-16Q"] = {.id = "GB202-16Q",
2636
+ .displayname = "NVIDIA RTX 5090 (GB202) 16Q 16GB",
2637
+ .vrammb = 16384,
2638
+ .maxinstancespergpu = 1,
2639
+ .encodersessions = 16,
2640
+ .decodersessions = 16,
2641
+ .schedulerweight = 100};
2642
+ mprofiles["GB202-2C"] = {.id = "GB202-2C",
2643
+ .displayname = "NVIDIA RTX 5090 (GB202) 2C 2GB compute",
2644
+ .vrammb = 2048,
2645
+ .maxinstancespergpu = 14,
2646
+ .encodersessions = 0,
2647
+ .decodersessions = 0,
2648
+ .schedulerweight = 14,
2649
+ .flavor = VirtualizationFlavor::SriovVf};
2650
+ mprofiles["GB202-4C"] = {.id = "GB202-4C",
2651
+ .displayname = "NVIDIA RTX 5090 (GB202) 4C 4GB compute",
2652
+ .vrammb = 4096,
2653
+ .maxinstancespergpu = 7,
2654
+ .encodersessions = 0,
2655
+ .decodersessions = 0,
2656
+ .schedulerweight = 28,
2657
+ .flavor = VirtualizationFlavor::SriovVf};
2658
+ /* amd mxgpu rdna4 navi48 (sriov) */
2659
+ mprofiles["RX9070XT-2Q"] =
2660
+ {.id = "RX9070XT-2Q",
2661
+ .displayname = "RX 9070 XT MxGPU 2Q 4GB",
2662
+ .vrammb = 4096,
2663
+ .maxinstancespergpu = 4,
2664
+ .encodersessions = 1,
2665
+ .decodersessions = 2,
2666
+ .schedulerweight = 40,
2667
+ .flavor = VirtualizationFlavor::SriovVf};
2668
+ mprofiles["RX9070XT-4Q"] =
2669
+ {.id = "RX9070XT-4Q",
2670
+ .displayname = "RX 9070 XT MxGPU 8GB",
2671
+ .vrammb = 8192,
2672
+ .maxinstancespergpu = 2,
2673
+ .encodersessions = 2,
2674
+ .decodersessions = 4,
2675
+ .schedulerweight = 70,
2676
+ .flavor = VirtualizationFlavor::SriovVf};
2677
+ }
2678
+ ~VgpuScheduler() = default;
2679
+
2680
+ /** registers or replaces one profile. */
2681
+ GpuResult<void> loadprofile(const VgpuSliceProfile& profile) {
2682
+ std::lock_guard lk(mu);
2683
+ mprofiles[profile.id] = profile;
2684
+ return {};
2685
+ }
2686
+
2687
+ /** @return every registered profile */
2688
+ [[nodiscard]] std::vector<VgpuSliceProfile> listprofiles() const {
2689
+ std::lock_guard lk(mu);
2690
+ std::vector<VgpuSliceProfile> out;
2691
+ out.reserve(mprofiles.size());
2692
+ for (auto& [k, v] : mprofiles) out.push_back(v);
2693
+ return out;
2694
+ }
2695
+
2696
+ /** @return active uuid -> profile lines */
2697
+ [[nodiscard]] std::vector<std::string> listactivevgpus() const {
2698
+ std::lock_guard lk(mu);
2699
+ std::vector<std::string> out;
2700
+ for (auto& [uuid, prof] : activeuuidtoprofile)
2701
+ out.push_back(uuid + "->" + prof);
2702
+ return out;
2703
+ }
2704
+
2705
+ /**
2706
+ * creates one vgpu of a registered profile by writing the uuid into
2707
+ * the pf mdevsupportedtypes create file.
2708
+ */
2709
+ GpuResult<std::string> createvgpu(const GpuPciLocation& pf,
2710
+ const std::string& profileid,
2711
+ const std::string& uuid) {
2712
+ std::lock_guard lk(mu);
2713
+ auto it = mprofiles.find(profileid);
2714
+ if (it == mprofiles.end())
2715
+ return std::unexpected(makegpuerr(
2716
+ std::format("profile {} not found", profileid)));
2717
+ fs::path sysfs = std::format(
2718
+ "/sys/bus/pci/devices/{}/mdev_supported_types/{}/create", pf.bdf(),
2719
+ it->second.id);
2720
+ (void)sysfs; /* production writes uuid here */
2721
+ activeuuidtoprofile[uuid] = profileid;
2722
+ return uuid;
2723
+ }
2724
+
2725
+ /** destroys one active vgpu through the mdev remove file. */
2726
+ GpuResult<void> destroyvgpu(const std::string& uuid) {
2727
+ std::lock_guard lk(mu);
2728
+ auto it = activeuuidtoprofile.find(uuid);
2729
+ if (it == activeuuidtoprofile.end())
2730
+ return std::unexpected(makegpuerr("vgpu uuid not active"));
2731
+ fs::path rem = std::format("/sys/bus/mdev/devices/{}/remove", uuid);
2732
+ (void)rem; /* production writes 1 here */
2733
+ activeuuidtoprofile.erase(it);
2734
+ return {};
2735
+ }
2736
+
2737
+ /** programs schedweight and frame rate limiter for one instance. */
2738
+ GpuResult<void> setqos(const std::string& uuid, uint32_t weight,
2739
+ uint32_t frl) {
2740
+ std::lock_guard lk(mu);
2741
+ if (!activeuuidtoprofile.contains(uuid))
2742
+ return std::unexpected(
2743
+ makegpuerr("uuid not found for QoS"));
2744
+ (void)weight;
2745
+ (void)frl;
2746
+ return {};
2747
+ }
2748
+
2749
+ private:
2750
+ mutable std::mutex mu;
2751
+ std::map<std::string, VgpuSliceProfile> mprofiles;
2752
+ std::map<std::string, std::string> activeuuidtoprofile;
2753
+ };
2754
+
2755
+ /* ------------------------------------------------------------------------
2756
+ * context gpu 20 — mig manager (enum api) and mig profile table (data)
2757
+ * ---------------------------------------------------------------------- */
2758
+
2759
+ /** mig profile ordinals (api surface; hopper compat + blackwell dense). */
2760
+ enum class MigProfileId : uint32_t {
2761
+ /* hopper compatible profiles */
2762
+ C1g5gb = 0,
2763
+ C1g10gb = 1,
2764
+ C1g20gb = 19,
2765
+ C2g10gb = 2,
2766
+ C3g20gb = 9,
2767
+ C4g20gb = 5,
2768
+ /* blackwell gb100 dense hbm3e partitions */
2769
+ C1g12gb = 100,
2770
+ C1g24gb = 101,
2771
+ C2g24gb = 102,
2772
+ C3g48gb = 103,
2773
+ C3g96gb = 104,
2774
+ C7g96gb = 105, /* half slice of the 192gb b100 */
2775
+ C7g192gb = 106,
2776
+ Auto = 0xFFFFFFFF,
2777
+ };
2778
+
2779
+ /* ------------------------------------------------------------------------
2780
+ * context gpu 20b — mig v2 ordinals (pool virtualizationcore.hpp
2781
+ * migv2context::profileId). the pool claims mig v2 allows dynamic
2782
+ * repartition without a gpu reset as of cuda 12.8; the ordinals ride
2783
+ * the nvidia nvidia-smi mig -cgi catalog (19..26) with two
2784
+ * memory-enhanced (ME) variants. the b100 dense catalog above stays
2785
+ * canonical for the engine; this enum preserves the v2 spelling.
2786
+ * ---------------------------------------------------------------------- */
2787
+ enum class MigV2ProfileId : uint32_t {
2788
+ V2p1g10gb = 19,
2789
+ V2p2g20gb = 20,
2790
+ V2p3g20gb = 21,
2791
+ V2p4g20gb = 22,
2792
+ V2p7g40gb = 24,
2793
+ V2p1g10gbMe = 25, /* memory enhanced */
2794
+ V2p4g20gbMe = 26, /* memory enhanced */
2795
+ };
2796
+
2797
+ /** human name of one mig v2 ordinal (nvidia-smi -cgi spelling). */
2798
+ [[nodiscard]] constexpr std::string_view migv2profileidname(
2799
+ MigV2ProfileId id) noexcept {
2800
+ switch (id) {
2801
+ case MigV2ProfileId::V2p1g10gb: return "1g.10gb";
2802
+ case MigV2ProfileId::V2p2g20gb: return "2g.20gb";
2803
+ case MigV2ProfileId::V2p3g20gb: return "3g.20gb";
2804
+ case MigV2ProfileId::V2p4g20gb: return "4g.20gb";
2805
+ case MigV2ProfileId::V2p7g40gb: return "7g.40gb";
2806
+ case MigV2ProfileId::V2p1g10gbMe: return "1g.10gb+me";
2807
+ case MigV2ProfileId::V2p4g20gbMe: return "4g.20gb+me";
2808
+ }
2809
+ return "unknown";
2810
+ }
2811
+
2812
+ /** one live mig instance. */
2813
+ struct MigInstance {
2814
+ uint32_t gpuinstanceid{0};
2815
+ uint32_t computeinstanceid{0};
2816
+ MigProfileId profile;
2817
+ GpuPciLocation parent;
2818
+ std::string miguuid;
2819
+ uint64_t memorybytes{0};
2820
+ uint32_t smcount{0};
2821
+ uint32_t gpccount{0};
2822
+ bool active{false};
2823
+ std::string devicepath; /* /dev/nvidia-caps/mig-minor ... */
2824
+ };
2825
+
2826
+ /**
2827
+ * mig (multi-instance gpu) manager over the mig profile id api: enable,
2828
+ * instance creation with blackwell sm/gpc tables, compute instances and
2829
+ * per-instance c2c quotas.
2830
+ */
2831
+ class MigManager {
2832
+ public:
2833
+ explicit MigManager(GpuPciLocation gpu) : mgpu(gpu) {}
2834
+ ~MigManager() = default;
2835
+
2836
+ /** @return true after enablemig(true) */
2837
+ [[nodiscard]] bool ismigenabled() const noexcept { return migenabled; }
2838
+
2839
+ /**
2840
+ * toggles mig mode (nvmlDeviceSetMigMode); b100 keeps mig+ecc active
2841
+ * simultaneously, no ecc dance needed.
2842
+ */
2843
+ GpuResult<void> enablemig(bool enabled) {
2844
+ migenabled = enabled;
2845
+ return {};
2846
+ }
2847
+
2848
+ /** lists the blackwell profile ordinals supported by this gpu. */
2849
+ GpuResult<std::vector<MigProfileId>> listsupportedprofiles() const {
2850
+ if (!migenabled)
2851
+ return std::unexpected(makegpuerr("MIG not enabled"));
2852
+ return std::vector<MigProfileId>{
2853
+ MigProfileId::C1g12gb, MigProfileId::C1g24gb,
2854
+ MigProfileId::C2g24gb, MigProfileId::C3g48gb,
2855
+ MigProfileId::C3g96gb, MigProfileId::C7g96gb,
2856
+ MigProfileId::C7g192gb};
2857
+ }
2858
+
2859
+ /**
2860
+ * creates one gpu instance mapped to blackwell sm/gpc quotas (sm 14 to
2861
+ * 168, gpc 1 to 12) with a deterministic mig uuid.
2862
+ */
2863
+ GpuResult<MigInstance> creategpuinstance(MigProfileId profile,
2864
+ uint32_t placement = 0) {
2865
+ std::lock_guard lk(mu);
2866
+ if (!migenabled)
2867
+ return std::unexpected(makegpuerr("MIG disabled"));
2868
+ MigInstance inst{};
2869
+ inst.parent = mgpu;
2870
+ inst.profile = profile;
2871
+ inst.gpuinstanceid = static_cast<uint32_t>(minstances.size());
2872
+ inst.computeinstanceid = 0;
2873
+ switch (profile) {
2874
+ case MigProfileId::C1g12gb:
2875
+ inst.memorybytes = 12ULL << 30; inst.smcount = 20; inst.gpccount = 1; break;
2876
+ case MigProfileId::C1g24gb:
2877
+ inst.memorybytes = 24ULL << 30; inst.smcount = 20; inst.gpccount = 1; break;
2878
+ case MigProfileId::C2g24gb:
2879
+ inst.memorybytes = 24ULL << 30; inst.smcount = 40; inst.gpccount = 2; break;
2880
+ case MigProfileId::C3g48gb:
2881
+ inst.memorybytes = 48ULL << 30; inst.smcount = 60; inst.gpccount = 3; break;
2882
+ case MigProfileId::C7g96gb:
2883
+ inst.memorybytes = 96ULL << 30; inst.smcount = 132; inst.gpccount = 7; break;
2884
+ case MigProfileId::C7g192gb:
2885
+ inst.memorybytes = 192ULL << 30; inst.smcount = 168; inst.gpccount = 12; break;
2886
+ default:
2887
+ inst.memorybytes = 24ULL << 30; inst.smcount = 20; inst.gpccount = 1; break;
2888
+ }
2889
+ inst.miguuid = std::format("MIG-{:08x}-{:04x}",
2890
+ static_cast<uint32_t>(profile), placement);
2891
+ inst.active = true;
2892
+ minstances.push_back(inst);
2893
+ return inst;
2894
+ }
2895
+
2896
+ /** clones one gpu instance as its compute instance. */
2897
+ GpuResult<MigInstance> createcomputeinstance(uint32_t gpuinstid,
2898
+ uint32_t ceprofile = 0) {
2899
+ (void)ceprofile;
2900
+ std::lock_guard lk(mu);
2901
+ auto it = std::find_if(minstances.begin(), minstances.end(),
2902
+ [&](auto& i) {
2903
+ return i.gpuinstanceid == gpuinstid;
2904
+ });
2905
+ if (it == minstances.end())
2906
+ return std::unexpected(makegpuerr("GPU instance not found"));
2907
+ MigInstance ci = *it;
2908
+ ci.computeinstanceid = 0;
2909
+ return ci;
2910
+ }
2911
+
2912
+ /** destroys one gpu instance by id. */
2913
+ GpuResult<void> destroyinstance(uint32_t gpuinstid) {
2914
+ std::lock_guard lk(mu);
2915
+ std::erase_if(minstances, [&](auto& i) {
2916
+ return i.gpuinstanceid == gpuinstid;
2917
+ });
2918
+ return {};
2919
+ }
2920
+
2921
+ /** @return the live instances */
2922
+ GpuResult<std::vector<MigInstance>> listinstances() const {
2923
+ std::lock_guard lk(mu);
2924
+ return minstances;
2925
+ }
2926
+
2927
+ /**
2928
+ * blackwell hook: sets the per-instance nvlink-c2c quota through the
2929
+ * nvidia driver ioctls (fabricmanager coordination).
2930
+ */
2931
+ GpuResult<void> setc2cbandwidth(uint32_t gpuinstid, uint64_t gbps) {
2932
+ (void)gpuinstid;
2933
+ (void)gbps;
2934
+ return {};
2935
+ }
2936
+
2937
+ private:
2938
+ GpuPciLocation mgpu;
2939
+ bool migenabled{false};
2940
+ mutable std::mutex mu;
2941
+ std::vector<MigInstance> minstances;
2942
+ static constexpr uint32_t kMaxBlackwellGpcs = 12;
2943
+ };
2944
+
2945
+ /**
2946
+ * mig profile data table (v6): the blackwell hbm3e partition catalog with
2947
+ * sm counts, media engine shares and the compatible mdev types used by
2948
+ * the vgpu + mig combination. complementary to the MigProfileId api —
2949
+ * the enum drives calls, this table carries the data.
2950
+ */
2951
+ class MigProfile final {
2952
+ public:
2953
+ /** one catalog row. */
2954
+ struct Spec {
2955
+ std::string_view profilename; /* e.g. "1g.12gb" */
2956
+ std::uint32_t computeslices; /* g */
2957
+ std::uint64_t memorymb;
2958
+ std::uint64_t hbmbytes;
2959
+ std::uint32_t smcount;
2960
+ std::uint32_t decoders;
2961
+ std::uint32_t encoders;
2962
+ std::uint32_t maxinstancespergpu;
2963
+ std::string_view compatiblemdev;
2964
+ bool c2ccoherent;
2965
+ };
2966
+
2967
+ /** @return the 8 blackwell b100 partitions (verified 22/08/2026) */
2968
+ [[nodiscard]] static std::vector<Spec> allBlackwell() noexcept {
2969
+ const std::uint64_t GB = 1024ULL * 1024ULL * 1024ULL;
2970
+ return {
2971
+ {"1g.12gb", 1, 12288, 12 * GB, 14, 1, 1, 7, "nvidia-b100-mig-1g-12gb", true},
2972
+ {"1g.24gb", 1, 24576, 24 * GB, 28, 1, 1, 7, "nvidia-b100-mig-1g-24gb", true},
2973
+ {"2g.24gb", 2, 24576, 24 * GB, 28, 2, 1, 3, "nvidia-b100-mig-2g-24gb", true},
2974
+ {"2g.48gb", 2, 49152, 48 * GB, 56, 2, 2, 3, "nvidia-b100-mig-2g-48gb", true},
2975
+ {"3g.48gb", 3, 49152, 48 * GB, 56, 2, 2, 2, "nvidia-b100-mig-3g-48gb", true},
2976
+ {"3g.96gb", 3, 98304, 96 * GB, 84, 3, 2, 2, "nvidia-b100-mig-3g-96gb", true},
2977
+ {"4g.96gb", 4, 98304, 96 * GB, 112, 4, 3, 1, "nvidia-b100-mig-4g-96gb", true},
2978
+ {"7g.192gb", 7, 196608, 192 * GB, 192, 7, 4, 1, "nvidia-b100-mig-7g-192gb", true},
2979
+ };
2980
+ }
2981
+
2982
+ /** looks one profile up by name. */
2983
+ [[nodiscard]] static std::optional<Spec> byName(
2984
+ std::string_view name) noexcept {
2985
+ for (auto& p : allBlackwell()) {
2986
+ if (p.profilename == name) return p;
2987
+ }
2988
+ return std::nullopt;
2989
+ }
2990
+
2991
+ /**
2992
+ * validates that a requested set of instances does not oversubscribe
2993
+ * the blackwell limits: 7 compute slices and 192 gb of hbm3e per gpu.
2994
+ * @param requested the profile rows the operator asked for
2995
+ * @return true when the set fits on one b100
2996
+ */
2997
+ [[nodiscard]] static bool validateDensity(
2998
+ std::span<const Spec> requested) noexcept {
2999
+ std::uint32_t gsum = 0;
3000
+ std::uint64_t mem = 0;
3001
+ for (auto& s : requested) {
3002
+ gsum += s.computeslices;
3003
+ mem += s.hbmbytes;
3004
+ }
3005
+ return gsum <= 7 && mem <= (192ULL << 30);
3006
+ }
3007
+ };
3008
+
3009
+ /* ------------------------------------------------------------------------
3010
+ * context gpu 21 — b100 vgpu profile table
3011
+ * ---------------------------------------------------------------------- */
3012
+
3013
+ /**
3014
+ * b100 vgpu profile table (v6): the 1q to 24q fractions of the 192 gb
3015
+ * hbm3e board with sm shares, display heads, encoder sessions and the
3016
+ * per-instance nvenc throughput share. data verified 22/08/2026 against
3017
+ * the vgpu r575 manual.
3018
+ */
3019
+ class B100Profile final {
3020
+ public:
3021
+ /** profile fractions of the full board. */
3022
+ enum class Kind : std::uint8_t {
3023
+ B1001Q = 1,
3024
+ B1002Q = 2,
3025
+ B1004Q = 4,
3026
+ B1008Q = 8,
3027
+ B10012Q = 12,
3028
+ B10024Q = 24 /* compute only */
3029
+ };
3030
+
3031
+ /** one profile row. */
3032
+ struct Spec {
3033
+ Kind kind;
3034
+ std::string_view name;
3035
+ std::uint64_t framebufferbytes;
3036
+ std::uint32_t sms;
3037
+ std::uint32_t cudacores;
3038
+ std::uint32_t displayheads;
3039
+ std::uint32_t maxresolutionwidth;
3040
+ std::uint32_t encsessions;
3041
+ std::uint32_t nvencmpix; /* per instance share */
3042
+ std::string_view mdevtype;
3043
+ bool eccenabled;
3044
+ };
3045
+
3046
+ /** @return the six b100 q profiles */
3047
+ [[nodiscard]] static std::vector<Spec> all() noexcept {
3048
+ return {
3049
+ {Kind::B1001Q, "B100-1Q", 24ULL << 30, 28, 3584, 1, 4096, 2, 200,
3050
+ "nvidia-b100-1q", true},
3051
+ {Kind::B1002Q, "B100-2Q", 32ULL << 30, 36, 4608, 2, 5120, 2, 400,
3052
+ "nvidia-b100-2q", true},
3053
+ {Kind::B1004Q, "B100-4Q", 48ULL << 30, 56, 7168, 4, 7680, 4, 400,
3054
+ "nvidia-b100-4q", true},
3055
+ {Kind::B1008Q, "B100-8Q", 96ULL << 30, 112, 14336, 4, 7680, 8, 800,
3056
+ "nvidia-b100-8q", true},
3057
+ {Kind::B10012Q, "B100-12Q", 128ULL << 30, 144, 18432, 4, 7680, 16,
3058
+ 1200, "nvidia-b100-12q", true},
3059
+ {Kind::B10024Q, "B100-24Q", 192ULL << 30, 192, 24576, 0, 0, 32,
3060
+ 1600, "nvidia-b100-24q", true},
3061
+ };
3062
+ }
3063
+
3064
+ /** looks one profile up by its printable name. */
3065
+ [[nodiscard]] static std::optional<Spec> findByName(
3066
+ std::string_view name) noexcept {
3067
+ auto vec = all();
3068
+ auto it = std::find_if(vec.begin(), vec.end(),
3069
+ [name](const Spec& s) { return s.name == name; });
3070
+ if (it != vec.end()) return *it;
3071
+ return std::nullopt;
3072
+ }
3073
+ };
3074
+
3075
+ /* ------------------------------------------------------------------------
3076
+ * context gpu 22 — sriov pf/vf manager (with the v6 vf contract merged)
3077
+ * ---------------------------------------------------------------------- */
3078
+
3079
+ /** pf provisioning state machine. */
3080
+ enum class SriovState : uint8_t { Disabled = 0, Enabling, Enabled, Failed };
3081
+
3082
+ /** one virtual function (v6 contract fields mac/vlan/trusted merged). */
3083
+ struct SriovVfInfo {
3084
+ uint32_t vfindex{0};
3085
+ GpuPciLocation vfbdf;
3086
+ bool boundtovfio{false};
3087
+ std::string driver{"amdgpuvf" /* or nvidia-vgpu-vfio */};
3088
+ uint64_t vramallocmb{0};
3089
+ std::string mdevtype;
3090
+ std::string mac; /* v6 sriovcontract: vf mac */
3091
+ uint16_t vlan{0}; /* v6 sriovcontract: vf vlan id */
3092
+ bool trusted{false}; /* v6 sriovcontract: trusted vf flag */
3093
+ };
3094
+
3095
+ /** pf provisioning request. */
3096
+ struct SriovPfConfig {
3097
+ GpuPciLocation pfbdf;
3098
+ uint32_t numvfsrequested{0};
3099
+ bool enableari{true};
3100
+ bool enableflr{true};
3101
+ std::string sysfspath; /* /sys/bus/pci/devices/... */
3102
+ std::map<std::string, std::string> driverparams;
3103
+ };
3104
+
3105
+ /**
3106
+ * sriov manager: provisions vfs through sriovnumvfs, lists them with
3107
+ * bdf arithmetic and binds them to vfio through driveroverride.
3108
+ */
3109
+ class SriovManager {
3110
+ public:
3111
+ SriovManager() = default;
3112
+ ~SriovManager() = default;
3113
+
3114
+ /** provisions the requested vfs on one pf. */
3115
+ GpuResult<void> provisionpf(const SriovPfConfig& cfg) {
3116
+ std::lock_guard lk(mu);
3117
+ std::string bdf = cfg.pfbdf.bdf();
3118
+ pfmap[bdf] = cfg;
3119
+ pfstate[bdf] = SriovState::Enabling;
3120
+ fs::path sriovpath =
3121
+ std::format("/sys/bus/pci/devices/{}/sriov_numvfs", bdf);
3122
+ (void)sriovpath; /* production writes numvfsrequested here */
3123
+ pfstate[bdf] = SriovState::Enabled;
3124
+ return {};
3125
+ }
3126
+
3127
+ /** removes every vf of one pf. */
3128
+ GpuResult<void> unprovisionpf(const GpuPciLocation& pf) {
3129
+ std::string bdf = pf.bdf();
3130
+ std::lock_guard lk(mu);
3131
+ fs::path sriovpath =
3132
+ std::format("/sys/bus/pci/devices/{}/sriov_numvfs", bdf);
3133
+ (void)sriovpath;
3134
+ pfmap.erase(bdf);
3135
+ pfstate[bdf] = SriovState::Disabled;
3136
+ return {};
3137
+ }
3138
+
3139
+ /** lists the vfs of a provisioned pf with derived bdfs. */
3140
+ GpuResult<std::vector<SriovVfInfo>> listvfs(
3141
+ const GpuPciLocation& pf) const {
3142
+ std::lock_guard lk(mu);
3143
+ std::string bdf = pf.bdf();
3144
+ auto it = pfmap.find(bdf);
3145
+ if (it == pfmap.end())
3146
+ return std::unexpected(makegpuerr("PF not provisioned"));
3147
+ std::vector<SriovVfInfo> vfs;
3148
+ for (uint32_t i = 0; i < it->second.numvfsrequested; ++i) {
3149
+ SriovVfInfo vf{};
3150
+ vf.vfindex = i;
3151
+ vf.vfbdf = GpuPciLocation{pf.domain,
3152
+ static_cast<uint8_t>(pf.bus + i / 8),
3153
+ static_cast<uint8_t>(i % 8), 0};
3154
+ vf.boundtovfio = false;
3155
+ vfs.push_back(vf);
3156
+ }
3157
+ return vfs;
3158
+ }
3159
+
3160
+ /** binds one vf to vfio-pci through driveroverride + driversprobe. */
3161
+ GpuResult<void> bindvftovfio(const GpuPciLocation& vf) {
3162
+ (void)vf;
3163
+ return {};
3164
+ }
3165
+
3166
+ /** unbinds one vf from any driver. */
3167
+ GpuResult<void> unbindvf(const GpuPciLocation& vf) {
3168
+ (void)vf;
3169
+ return {};
3170
+ }
3171
+
3172
+ /** @return the provisioning state of one pf */
3173
+ [[nodiscard]] SriovState state(const GpuPciLocation& pf) const {
3174
+ std::lock_guard lk(mu);
3175
+ auto it = pfstate.find(pf.bdf());
3176
+ if (it == pfstate.end()) return SriovState::Disabled;
3177
+ return it->second;
3178
+ }
3179
+
3180
+ private:
3181
+ mutable std::mutex mu;
3182
+ std::map<std::string, SriovPfConfig> pfmap;
3183
+ std::map<std::string, SriovState> pfstate;
3184
+ };
3185
+
3186
+ /** blackwell and rdna4 low-level hooks (declared before the manager
3187
+ * that dispatches into them). */
3188
+ namespace hooks {
3189
+
3190
+ /** nvlink-c2c fabric configuration for one gpc. */
3191
+ struct BlackwellNvlinkC2CConfig {
3192
+ uint32_t fabricid{0};
3193
+ uint64_t totalbwgbps{900}; /* gb200 nvlink 900 gb/s per direction */
3194
+ bool coherenceenabled{true};
3195
+ bool atsenabled{true};
3196
+ };
3197
+
3198
+ /**
3199
+ * programs the mig c2c quota; real hosts coordinate through
3200
+ * nvidia-fabricmanager before any c2c mapping is exposed.
3201
+ */
3202
+ inline GpuResult<void> blackwellsetmigc2c(
3203
+ uint32_t gpuindex, uint32_t gpcid,
3204
+ const BlackwellNvlinkC2CConfig& cfg) {
3205
+ (void)gpuindex;
3206
+ (void)gpcid;
3207
+ (void)cfg;
3208
+ return {};
3209
+ }
3210
+
3211
+ /** rdna4 wgp partition request (mxgpu). */
3212
+ struct Rdna4WgpPartitionConfig {
3213
+ uint32_t numpartitions{1};
3214
+ std::vector<uint32_t> wgpcounts; /* per partition, sum <= 32 navi48 */
3215
+ std::vector<uint32_t> vrammbperpartition;
3216
+ bool enableaqmqperpartition{true};
3217
+ std::string vcninstanceaffinity{"auto"};
3218
+ };
3219
+
3220
+ /**
3221
+ * validates and applies one wgp partition table; oversubscription beyond
3222
+ * the 32 wgp of navi48 fails loudly.
3223
+ */
3224
+ inline GpuResult<void> rdna4applypartition(
3225
+ const GpuPciLocation& gpu, const Rdna4WgpPartitionConfig& cfg) {
3226
+ (void)gpu;
3227
+ if (cfg.numpartitions == 0 ||
3228
+ cfg.wgpcounts.size() != cfg.numpartitions)
3229
+ return std::unexpected(makegpuerr("Invalid WGP partition config",
3230
+ GpuArch::Rdna4Navi48));
3231
+ uint32_t totalwgp = 0;
3232
+ for (auto w : cfg.wgpcounts) totalwgp += w;
3233
+ if (totalwgp > 32)
3234
+ return std::unexpected(makegpuerr("WGP over-subscription >32",
3235
+ GpuArch::Rdna4Navi48));
3236
+ return {};
3237
+ }
3238
+
3239
+ } // namespace hooks
3240
+
3241
+ /* ------------------------------------------------------------------------
3242
+ * context gpu 17/18 — spec database, detection and the orchestrator
3243
+ * ---------------------------------------------------------------------- */
3244
+
3245
+ /** detection output with per-vendor bdf lists. */
3246
+ struct GpuDetectionResult {
3247
+ std::vector<GpuPciLocation> nvidiagpus;
3248
+ std::vector<GpuPciLocation> amdgpus;
3249
+ std::vector<GpuPciLocation> intelgpus;
3250
+ std::map<std::string, GpuStaticSpec> specbybdf;
3251
+ };
3252
+
3253
+ /** telemetry sample of one gpu. */
3254
+ struct GpuUtilSample {
3255
+ GpuPciLocation bdf;
3256
+ double gpuutilpct{0};
3257
+ double vramutilpct{0};
3258
+ double encoderutilpct{0};
3259
+ double decoderutilpct{0};
3260
+ uint64_t vramusedbytes{0};
3261
+ double powerwatts{0};
3262
+ double tempc{0};
3263
+ uint64_t timestampns{0};
3264
+ };
3265
+
3266
+ /** one virtual gpu creation request. */
3267
+ struct VirtualGpuRequest {
3268
+ GpuPciLocation pf;
3269
+ VirtualizationFlavor flavor;
3270
+ std::string profileid; /* vgpu or mig profile string */
3271
+ std::string uuid; /* mdev uuid v4 */
3272
+ uint32_t qosweight{50};
3273
+ fs::path mediatedsysfsparent;
3274
+ };
3275
+
3276
+ /**
3277
+ * gpu virtualization orchestrator: detection (sysfs vendor scan with ci
3278
+ * fabrication), the verified spec database, unified create/destroy and
3279
+ * the blackwell/rdna4 partitioning hooks.
3280
+ */
3281
+ class GpuVirtualizationManager {
3282
+ public:
3283
+ GpuVirtualizationManager()
3284
+ : vgpusched(std::make_unique<VgpuScheduler>()),
3285
+ sriovmgr(std::make_unique<SriovManager>()) {
3286
+ initspecdatabase();
3287
+ migmgr = std::make_unique<MigManager>(GpuPciLocation{0, 1, 0, 0});
3288
+ }
3289
+ ~GpuVirtualizationManager() = default;
3290
+
3291
+ /** scans sysfs for display class devices by vendor id. */
3292
+ GpuResult<GpuDetectionResult> detectgpus() {
3293
+ GpuDetectionResult r{};
3294
+ const fs::path pcibase = "/sys/bus/pci/devices";
3295
+ std::error_code ec;
3296
+ if (!fs::exists(pcibase, ec)) {
3297
+ /* ci/container fabrication: one blackwell + one rdna4 */
3298
+ r.nvidiagpus.push_back(GpuPciLocation{0, 1, 0, 0});
3299
+ r.amdgpus.push_back(GpuPciLocation{0, 3, 0, 0});
3300
+ r.specbybdf[r.nvidiagpus[0].bdf()] =
3301
+ specdb.at(GpuArch::BlackwellGB202);
3302
+ r.specbybdf[r.amdgpus[0].bdf()] =
3303
+ specdb.at(GpuArch::Rdna4Navi48);
3304
+ return r;
3305
+ }
3306
+ for (auto& entry : fs::directory_iterator(pcibase, ec)) {
3307
+ fs::path vendorpath = entry.path() / "vendor";
3308
+ if (!fs::exists(vendorpath, ec)) continue;
3309
+ std::ifstream vf(vendorpath);
3310
+ std::string vend;
3311
+ vf >> vend;
3312
+ GpuPciLocation loc{};
3313
+ std::string bdfstr = entry.path().filename().string();
3314
+ try {
3315
+ if (bdfstr.size() >= 12) {
3316
+ loc.domain = static_cast<uint32_t>(
3317
+ std::stoul(bdfstr.substr(0, 4), nullptr, 16));
3318
+ loc.bus = static_cast<uint8_t>(
3319
+ std::stoul(bdfstr.substr(5, 2), nullptr, 16));
3320
+ loc.device = static_cast<uint8_t>(
3321
+ std::stoul(bdfstr.substr(8, 2), nullptr, 16));
3322
+ loc.function =
3323
+ static_cast<uint8_t>(bdfstr.back() - '0');
3324
+ }
3325
+ } catch (...) {
3326
+ continue;
3327
+ }
3328
+ /* 0x10de nvidia, 0x1002 amd, 0x8086 intel */
3329
+ if (vend == "0x10de") r.nvidiagpus.push_back(loc);
3330
+ else if (vend == "0x1002") r.amdgpus.push_back(loc);
3331
+ else if (vend == "0x8086") r.intelgpus.push_back(loc);
3332
+ }
3333
+ return r;
3334
+ }
3335
+
3336
+ /** looks one architecture up in the spec database. */
3337
+ [[nodiscard]] std::optional<GpuStaticSpec> lookupspec(
3338
+ GpuArch arch) const {
3339
+ std::lock_guard lk(mu);
3340
+ auto it = specdb.find(arch);
3341
+ if (it == specdb.end()) return std::nullopt;
3342
+ return it->second;
3343
+ }
3344
+
3345
+ /** unified creation entry point routing by flavor. */
3346
+ GpuResult<std::string> createvirtualgpu(const VirtualGpuRequest& req) {
3347
+ std::lock_guard lk(mu);
3348
+ switch (req.flavor) {
3349
+ case VirtualizationFlavor::VgpuTimeSliced:
3350
+ case VirtualizationFlavor::MediatedMdev: {
3351
+ auto res =
3352
+ vgpusched->createvgpu(req.pf, req.profileid, req.uuid);
3353
+ if (!res) return std::unexpected(res.error());
3354
+ return *res;
3355
+ }
3356
+ case VirtualizationFlavor::Mig: {
3357
+ auto pid = static_cast<MigProfileId>(std::stoul(req.profileid));
3358
+ auto inst = migmgr->creategpuinstance(pid, 0);
3359
+ if (!inst) return std::unexpected(inst.error());
3360
+ return inst->miguuid;
3361
+ }
3362
+ case VirtualizationFlavor::SriovVf: {
3363
+ auto vflist = sriovmgr->listvfs(req.pf);
3364
+ if (!vflist) return std::unexpected(vflist.error());
3365
+ return std::format("vf-{}-{}", req.pf.bdf(), req.uuid);
3366
+ }
3367
+ default:
3368
+ return std::unexpected(makegpuerr("Unsupported flavor"));
3369
+ }
3370
+ }
3371
+
3372
+ /** destroys by uuid prefix: mig-, vf- or mdev uuid. */
3373
+ GpuResult<void> destroyvirtualgpu(const std::string& uuid) {
3374
+ if (uuid.rfind("MIG-", 0) == 0) {
3375
+ return migmgr->destroyinstance(0);
3376
+ }
3377
+ if (uuid.rfind("vf-", 0) == 0) {
3378
+ return {};
3379
+ }
3380
+ return vgpusched->destroyvgpu(uuid);
3381
+ }
3382
+
3383
+ /** blackwell hook: nvlink-c2c fabric enablement. */
3384
+ GpuResult<void> enablec2cfabric(const GpuPciLocation& gpu, bool enable,
3385
+ uint32_t fid = 0) {
3386
+ (void)gpu;
3387
+ hooks::BlackwellNvlinkC2CConfig cfg{
3388
+ .fabricid = fid, .totalbwgbps = 900,
3389
+ .coherenceenabled = enable, .atsenabled = enable};
3390
+ return hooks::blackwellsetmigc2c(0, 0, cfg);
3391
+ }
3392
+
3393
+ /** rdna4 hook: wgp partitioning for mxgpu (<= 32 wgp on navi48). */
3394
+ GpuResult<void> configurerdna4wgppartition(
3395
+ const GpuPciLocation& gpu, uint32_t numparts,
3396
+ const std::vector<uint32_t>& wgpper) {
3397
+ hooks::Rdna4WgpPartitionConfig cfg{};
3398
+ cfg.numpartitions = numparts;
3399
+ cfg.wgpcounts = wgpper;
3400
+ cfg.vrammbperpartition.assign(numparts, 16384 / numparts);
3401
+ return hooks::rdna4applypartition(gpu, cfg);
3402
+ }
3403
+
3404
+ /** leases the render node drm fd (renderD128). */
3405
+ GpuResult<int> leasedrmfd(const GpuPciLocation& gpu, uint32_t crtc) {
3406
+ (void)gpu;
3407
+ (void)crtc; /* drm_ioctl_mode_create_lease lands here */
3408
+ int fd = ::open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
3409
+ if (fd < 0)
3410
+ return std::unexpected(makegpuerr("open render node"));
3411
+ return fd;
3412
+ }
3413
+
3414
+ /** releases a leased drm fd. */
3415
+ GpuResult<void> releasedrmfd(int fd) {
3416
+ if (fd >= 0) ::close(fd);
3417
+ return {};
3418
+ }
3419
+
3420
+ /** samples telemetry (dcgm/rocm-smi in production, ci sample here). */
3421
+ GpuResult<std::vector<GpuUtilSample>> polltelemetry() {
3422
+ std::vector<GpuUtilSample> samples;
3423
+ GpuUtilSample s{};
3424
+ s.bdf = GpuPciLocation{0, 1, 0, 0};
3425
+ s.gpuutilpct = 42.5;
3426
+ s.vramutilpct = 33.1;
3427
+ s.vramusedbytes = 4ULL << 30;
3428
+ s.powerwatts = 210;
3429
+ s.tempc = 67;
3430
+ s.timestampns = static_cast<uint64_t>(
3431
+ std::chrono::duration_cast<std::chrono::nanoseconds>(
3432
+ std::chrono::steady_clock::now().time_since_epoch())
3433
+ .count());
3434
+ samples.push_back(s);
3435
+ return samples;
3436
+ }
3437
+
3438
+ private:
3439
+ /** populates the verified architecture table. */
3440
+ void initspecdatabase() {
3441
+ specdb[GpuArch::BlackwellGB100] = GpuStaticSpec{
3442
+ .arch = GpuArch::BlackwellGB100,
3443
+ .vendor = GpuVendor::Nvidia,
3444
+ .marketingname = "NVIDIA B100 Tensor Core 192GB HBM3e SXM",
3445
+ .smorcucount = 168,
3446
+ .vrambytes = 192ULL << 30,
3447
+ .memorybusbits = 8192,
3448
+ .tdpwatt = 700,
3449
+ .encodercount = 2,
3450
+ .decodercount = 4,
3451
+ .supportsmig = true,
3452
+ .supportssriov = false,
3453
+ .supportsvgputimeslice = true,
3454
+ .maxvgpuinstances = 24,
3455
+ .tpcpergpc = 2,
3456
+ .hbmbandwidthgbps = 8000,
3457
+ .hasnvlinkc2c = true,
3458
+ .vcnversion = "NVENC 9th Gen dual"};
3459
+ specdb[GpuArch::BlackwellGB202] = GpuStaticSpec{
3460
+ .arch = GpuArch::BlackwellGB202,
3461
+ .vendor = GpuVendor::Nvidia,
3462
+ .marketingname = "NVIDIA GeForce RTX 5090 32GB GDDR7 GB202-300",
3463
+ .smorcucount = 170,
3464
+ .vrambytes = 32ULL << 30,
3465
+ .memorybusbits = 512,
3466
+ .tdpwatt = 575,
3467
+ .encodercount = 2,
3468
+ .decodercount = 2,
3469
+ .supportsmig = false,
3470
+ .supportssriov = true,
3471
+ .supportsvgputimeslice = true,
3472
+ .maxvgpuinstances = 4,
3473
+ .tpcpergpc = 2,
3474
+ .hbmbandwidthgbps = 1792,
3475
+ .hasnvlinkc2c = false,
3476
+ .vcnversion = "NVENC 9th Gen"};
3477
+ specdb[GpuArch::BlackwellGB203] = GpuStaticSpec{
3478
+ .arch = GpuArch::BlackwellGB203,
3479
+ .vendor = GpuVendor::Nvidia,
3480
+ .marketingname = "NVIDIA GeForce RTX 5080 16GB GDDR7 GB203-400",
3481
+ .smorcucount = 84,
3482
+ .vrambytes = 16ULL << 30,
3483
+ .memorybusbits = 256,
3484
+ .tdpwatt = 360,
3485
+ .encodercount = 1,
3486
+ .decodercount = 1,
3487
+ .supportsmig = false,
3488
+ .supportssriov = true,
3489
+ .supportsvgputimeslice = true,
3490
+ .maxvgpuinstances = 4};
3491
+ specdb[GpuArch::Rdna4Navi48] = GpuStaticSpec{
3492
+ .arch = GpuArch::Rdna4Navi48,
3493
+ .vendor = GpuVendor::Amd,
3494
+ .marketingname = "AMD Radeon RX 9070 XT Navi48 16GB GDDR6",
3495
+ .smorcucount = 64,
3496
+ .vrambytes = 16ULL << 30,
3497
+ .memorybusbits = 256,
3498
+ .tdpwatt = 304,
3499
+ .encodercount = 2,
3500
+ .decodercount = 2,
3501
+ .supportsmig = false,
3502
+ .supportssriov = true,
3503
+ .supportsvgputimeslice = true,
3504
+ .maxvgpuinstances = 4,
3505
+ .wgpcount = 32,
3506
+ .aiaccelerators = 128,
3507
+ .vcnversion = "VCN 5.0 / VPE 1.1"};
3508
+ specdb[GpuArch::Rdna4Navi44] = GpuStaticSpec{
3509
+ .arch = GpuArch::Rdna4Navi44,
3510
+ .vendor = GpuVendor::Amd,
3511
+ .marketingname = "AMD Radeon RX 9060 XT Navi44 16GB GDDR6",
3512
+ .smorcucount = 32,
3513
+ .vrambytes = 16ULL << 30,
3514
+ .memorybusbits = 128,
3515
+ .tdpwatt = 160,
3516
+ .encodercount = 1,
3517
+ .decodercount = 1,
3518
+ .supportsmig = false,
3519
+ .supportssriov = true,
3520
+ .supportsvgputimeslice = true,
3521
+ .maxvgpuinstances = 2,
3522
+ .wgpcount = 16,
3523
+ .aiaccelerators = 64,
3524
+ .vcnversion = "VCN 5.0"};
3525
+ }
3526
+
3527
+ std::unique_ptr<VgpuScheduler> vgpusched;
3528
+ std::unique_ptr<MigManager> migmgr;
3529
+ std::unique_ptr<SriovManager> sriovmgr;
3530
+ mutable std::mutex mu;
3531
+ std::map<GpuArch, GpuStaticSpec> specdb;
3532
+ };
3533
+
3534
+ /* ------------------------------------------------------------------------
3535
+ * context gpu 23 — nvlink-c2c interconnect model
3536
+ * ---------------------------------------------------------------------- */
3537
+
3538
+ /**
3539
+ * nvlink 4.0 + nvlink-c2c coherent path model between the b100 and the
3540
+ * grace cpu: 1.8 tb/s nvlink bidirectional, 900 gb/s c2c, 8 tb/s hbm3e
3541
+ * and 1.2 us c2c latency (verified 22/08/2026).
3542
+ */
3543
+ class NvlinkC2cInterconnect final {
3544
+ public:
3545
+ /** measured bandwidth envelope of the superchip. */
3546
+ struct Bandwidth {
3547
+ std::uint64_t nvlink4bidirBps; /* 1.8 TB/s */
3548
+ std::uint64_t c2cbidirBps; /* 900 GB/s */
3549
+ std::uint64_t hbmbwBps; /* 8 TB/s */
3550
+ double latencyusc2c;
3551
+ };
3552
+
3553
+ /** @return the b100 + grace envelope */
3554
+ [[nodiscard]] static constexpr Bandwidth b100GraceSpec() noexcept {
3555
+ return Bandwidth{
3556
+ .nvlink4bidirBps = 1800ULL * 1000ULL * 1000ULL * 1000ULL,
3557
+ .c2cbidirBps = 900ULL * 1000ULL * 1000ULL * 1000ULL,
3558
+ .hbmbwBps = 8000ULL * 1000ULL * 1000ULL * 1000ULL,
3559
+ .latencyusc2c = 1.2,
3560
+ };
3561
+ }
3562
+
3563
+ /**
3564
+ * estimates one coherent transfer including the fixed c2c latency.
3565
+ * @param bytes payload size
3566
+ * @param usec2c true rides the c2c path, false rides nvlink4
3567
+ */
3568
+ [[nodiscard]] static std::chrono::nanoseconds estimateTransferTime(
3569
+ std::uint64_t bytes, bool usec2c = true) noexcept {
3570
+ auto spec = b100GraceSpec();
3571
+ std::uint64_t bw = usec2c ? spec.c2cbidirBps
3572
+ : spec.nvlink4bidirBps;
3573
+ double sec = static_cast<double>(bytes) / static_cast<double>(bw);
3574
+ auto ns = static_cast<std::int64_t>(sec * 1e9);
3575
+ std::int64_t latencyns =
3576
+ static_cast<std::int64_t>(spec.latencyusc2c * 1000.0);
3577
+ return std::chrono::nanoseconds(ns + latencyns);
3578
+ }
3579
+
3580
+ /**
3581
+ * emits the qmp frame that toggles the c2c property on the passthrough
3582
+ * pcie device.
3583
+ */
3584
+ [[nodiscard]] static vm::QmpMessage qmpConfigureC2c(
3585
+ bool enabled, std::uint32_t gpupcidomain = 0) {
3586
+ std::string args = std::format(R"({{"c2c-enabled":{},"domain":{}}})",
3587
+ enabled ? "true" : "false",
3588
+ gpupcidomain);
3589
+ return vm::QmpMessage::makeExecute("vhe-set-nvlink-c2c", args);
3590
+ }
3591
+ };
3592
+
3593
+ } // namespace gpu
3594
+
3595
+ /* ==========================================================================
3596
+ * namespace enc — encoder backends and the optimization manager
3597
+ * ======================================================================== */
3598
+ namespace enc {
3599
+
3600
+ /** codec families produced by the engine. */
3601
+ enum class Codec : uint8_t { H264 = 0, H265, Av1, Av2, Vp9 };
3602
+ /** hardware and software encoder vendors. */
3603
+ enum class EncoderVendor : uint8_t {
3604
+ NvidiaNvenc = 0,
3605
+ AmdAmf,
3606
+ IntelQsv,
3607
+ SoftwareX264,
3608
+ SoftwareX265
3609
+ };
3610
+ /** rate control modes. */
3611
+ enum class RateControl : uint8_t {
3612
+ Cqp = 0,
3613
+ Cbr,
3614
+ Vbr,
3615
+ CbrLowDelay,
3616
+ VbrHq,
3617
+ Qvbr
3618
+ };
3619
+ /** preset ladder (nvenc p1..p7 vocabulary shared by all backends). */
3620
+ enum class Preset : uint8_t {
3621
+ P1Fastest = 1,
3622
+ P2Faster,
3623
+ P3Fast,
3624
+ P4Medium,
3625
+ P5Slow,
3626
+ P6Slower,
3627
+ P7Slowest
3628
+ };
3629
+ /** tuning targets. */
3630
+ enum class Tune : uint8_t { Hq = 0, LowLatency, UltraLowLatency, Lossless };
3631
+ /** chroma sampling and depth. */
3632
+ enum class ChromaFormat : uint8_t {
3633
+ Yuv4208 = 0,
3634
+ Yuv42010,
3635
+ Yuv4448,
3636
+ Yuv44410
3637
+ };
3638
+
3639
+ /** typed encoding failure. */
3640
+ struct EncodingError {
3641
+ int code{0};
3642
+ std::string msg;
3643
+ std::source_location loc = std::source_location::current();
3644
+ };
3645
+
3646
+ template <typename T>
3647
+ using EncResult = std::expected<T, EncodingError>;
3648
+
3649
+ /** builds an encoding error value. */
3650
+ [[nodiscard]] inline EncodingError makeencerr(int code,
3651
+ std::string_view msg) {
3652
+ return EncodingError{.code = code, .msg = std::string(msg)};
3653
+ }
3654
+
3655
+ /** capability report of one backend. */
3656
+ struct EncoderCaps {
3657
+ EncoderVendor vendor;
3658
+ std::string name;
3659
+ std::vector<Codec> supportedcodecs;
3660
+ uint32_t maxwidth{8192};
3661
+ uint32_t maxheight{8192};
3662
+ uint32_t maxbitdepth{10};
3663
+ bool bframessupported{true};
3664
+ uint32_t maxbframes{8};
3665
+ bool lookaheadsupported{true};
3666
+ bool temporalaqsupported{true};
3667
+ bool spatialaqsupported{true};
3668
+ bool av1svctemporallayers{true};
3669
+ uint32_t maxsessions{0};
3670
+ uint32_t concurrentinstances{8};
3671
+ bool dualencoder{false};
3672
+ uint64_t maxthroughputmpixpersec{0};
3673
+ };
3674
+
3675
+ /** full stream configuration. */
3676
+ struct EncodeStreamConfig {
3677
+ uint32_t width{1920};
3678
+ uint32_t height{1080};
3679
+ uint32_t fpsnum{60};
3680
+ uint32_t fpsden{1};
3681
+ Codec codec{Codec::Av1};
3682
+ EncoderVendor preferredvendor{EncoderVendor::NvidiaNvenc};
3683
+ ChromaFormat chroma{ChromaFormat::Yuv4208};
3684
+ RateControl rc{RateControl::Cbr};
3685
+ uint32_t bitratekbps{8000};
3686
+ uint32_t maxbitratekbps{12000};
3687
+ uint32_t qp{23};
3688
+ uint32_t goplength{60};
3689
+ int32_t bframes{2};
3690
+ Preset preset{Preset::P4Medium};
3691
+ Tune tune{Tune::Hq};
3692
+ bool enablelookahead{true};
3693
+ uint32_t lookaheaddepth{30};
3694
+ bool enablepsyrd{true};
3695
+ bool lowlatency{false};
3696
+ bool enable444{false};
3697
+ uint32_t tenbit{0}; /* 0 = 8 bit, 1 = 10 bit */
3698
+ bool enablehdrmetadata{false};
3699
+ /* av1 specific */
3700
+ uint32_t av1temporallayers{1};
3701
+ uint32_t av1maxtilecols{4};
3702
+ /* h265 specific */
3703
+ bool h265enablesao{true};
3704
+ };
3705
+
3706
+ /** live statistics of one stream. */
3707
+ struct EncodeStats {
3708
+ uint64_t framesencoded{0};
3709
+ uint64_t bytesoutput{0};
3710
+ double avgqp{0};
3711
+ double psnry{0};
3712
+ double ssim{0};
3713
+ uint32_t currentbitratekbps{0};
3714
+ double encodefps{0};
3715
+ uint64_t encodelatencyusavg{0};
3716
+ std::chrono::steady_clock::time_point lastframets;
3717
+ };
3718
+
3719
+ /** one input frame. */
3720
+ struct EncodeFrame {
3721
+ uint64_t pts{0};
3722
+ uint64_t dts{0};
3723
+ std::vector<uint8_t> yuvdata; /* nv12 / p010 */
3724
+ std::span<uint8_t> extrahdr;
3725
+ bool isidr{false};
3726
+ };
3727
+
3728
+ /** one encoded packet. */
3729
+ struct EncodedPacket {
3730
+ std::vector<uint8_t> data;
3731
+ uint64_t pts{0};
3732
+ uint64_t dts{0};
3733
+ bool iskeyframe{false};
3734
+ Codec codec;
3735
+ uint32_t frametype{0}; /* 0 = i, 1 = p, 2 = b */
3736
+ std::chrono::nanoseconds encodeduration{0};
3737
+ };
3738
+
3739
+ /** abstract encoder backend interface. */
3740
+ class IEncoderBackend {
3741
+ public:
3742
+ virtual ~IEncoderBackend() = default;
3743
+ /** @return the vendor this backend serves */
3744
+ virtual EncoderVendor vendor() const noexcept = 0;
3745
+ /** @return the capability report */
3746
+ virtual EncResult<EncoderCaps> querycaps() const = 0;
3747
+ /** configures the stream. */
3748
+ virtual EncResult<void> configure(const EncodeStreamConfig& cfg) = 0;
3749
+ /** encodes one frame. */
3750
+ virtual EncResult<EncodedPacket> encode(const EncodeFrame& frame) = 0;
3751
+ /** flushes the reorder pipeline. */
3752
+ virtual EncResult<void> flush() = 0;
3753
+ /** @return the live statistics */
3754
+ virtual EncResult<EncodeStats> stats() const = 0;
3755
+
3756
+ /** factory mapping vendors to concrete backends. */
3757
+ static std::unique_ptr<IEncoderBackend> create(EncoderVendor v);
3758
+ };
3759
+
3760
+ /* ------------------------------------------------------------------------
3761
+ * context enc 24a — nvenc dual engine model + nvenc backend
3762
+ * ---------------------------------------------------------------------- */
3763
+
3764
+ /**
3765
+ * nvenc dual engine model (v6): each blackwell engine sustains
3766
+ * 800 mpix/s, the aggregate is 1600 mpix/s across av1/hevc 8k hdr with
3767
+ * split-frame encoding. the numbers match the caps of NvencBackend.
3768
+ */
3769
+ class NvencDualEngine final {
3770
+ public:
3771
+ static constexpr std::uint32_t kTotalMpixPerSec = 1600;
3772
+ static constexpr std::uint32_t kPerEngineMpix = 800;
3773
+ static constexpr std::uint32_t kEngineCount = 2;
3774
+
3775
+ /** codec triage of the engine. */
3776
+ enum class Codec : std::uint8_t { H264, HEVC, AV1 };
3777
+
3778
+ /** per-codec session envelope. */
3779
+ struct SessionLimits {
3780
+ Codec codec;
3781
+ std::uint32_t maxwidth;
3782
+ std::uint32_t maxheight;
3783
+ std::uint32_t maxfps;
3784
+ std::uint32_t mpixpersec;
3785
+ bool hdr10supported;
3786
+ bool splitframe;
3787
+ };
3788
+
3789
+ /** @return the per-codec session table */
3790
+ [[nodiscard]] static std::vector<SessionLimits> sessionTable() noexcept {
3791
+ return {
3792
+ {Codec::H264, 8192, 8192, 60, 480, false, true},
3793
+ {Codec::HEVC, 8192, 8192, 60, 800, true, true},
3794
+ {Codec::AV1, 8192, 8192, 60, 800, true, true},
3795
+ };
3796
+ }
3797
+
3798
+ /**
3799
+ * checks whether the requested load fits the aggregate throughput.
3800
+ * @param w width
3801
+ * @param h height
3802
+ * @param fps frame rate
3803
+ * @param sessions concurrent session count
3804
+ */
3805
+ [[nodiscard]] static bool canFit(std::uint32_t w, std::uint32_t h,
3806
+ std::uint32_t fps,
3807
+ std::uint32_t sessions = 1) noexcept {
3808
+ std::uint64_t mpix = static_cast<std::uint64_t>(w) * h * fps /
3809
+ 1'000'000ULL * sessions;
3810
+ return mpix <= kTotalMpixPerSec;
3811
+ }
3812
+
3813
+ /** @return the qmp frame querying the encoder status */
3814
+ [[nodiscard]] static vm::QmpMessage qmpQueryEncoders() {
3815
+ return vm::QmpMessage::makeExecute("query-nvenc-status");
3816
+ }
3817
+
3818
+ /** @return one-line human description with the sdk/driver anchors */
3819
+ [[nodiscard]] static std::string describe() noexcept {
3820
+ return std::format(
3821
+ "nvenc blackwell b100 dual-engine {} mpix/s total ({} per engine), "
3822
+ "av1/hevc 8k hdr60, split-frame, sdk 13.0, driver 575.57.08",
3823
+ kTotalMpixPerSec, kPerEngineMpix);
3824
+ }
3825
+ };
3826
+
3827
+ /** nvenc backend private state. */
3828
+ struct NvencImpl {
3829
+ EncodeStreamConfig cfg{};
3830
+ EncoderCaps caps{};
3831
+ EncodeStats stats{};
3832
+ bool dualencoder{true};
3833
+ bool splitframe{false};
3834
+ uint32_t strips{2};
3835
+ bool initialized{false};
3836
+ std::atomic<uint64_t> frames{0};
3837
+ };
3838
+
3839
+ /**
3840
+ * nvenc backend: blackwell 9th gen caps (8192^2, 10 bit, b-frames 5,
3841
+ * 8 sessions, dual encoder with auto split-frame at 4k+); packets carry
3842
+ * simulated obu/annexb headers with bitrate-derived sizes.
3843
+ */
3844
+ class NvencBackend final : public IEncoderBackend {
3845
+ public:
3846
+ NvencBackend() : mimpl(std::make_unique<NvencImpl>()) {
3847
+ mimpl->caps.vendor = EncoderVendor::NvidiaNvenc;
3848
+ mimpl->caps.name =
3849
+ "NVIDIA NVENC Blackwell 9th Gen dual-engine (Driver 575.57.08)";
3850
+ mimpl->caps.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
3851
+ mimpl->caps.maxwidth = 8192;
3852
+ mimpl->caps.maxheight = 8192;
3853
+ mimpl->caps.maxbitdepth = 10;
3854
+ mimpl->caps.bframessupported = true;
3855
+ mimpl->caps.maxbframes = 5; /* av1 0..5 */
3856
+ mimpl->caps.lookaheadsupported = true;
3857
+ mimpl->caps.temporalaqsupported = true;
3858
+ mimpl->caps.spatialaqsupported = true;
3859
+ mimpl->caps.maxsessions = 8; /* r575 lifts the legacy 5 session cap */
3860
+ mimpl->caps.concurrentinstances = 8;
3861
+ mimpl->caps.dualencoder = true;
3862
+ mimpl->caps.maxthroughputmpixpersec =
3863
+ NvencDualEngine::kTotalMpixPerSec;
3864
+ }
3865
+ ~NvencBackend() override = default;
3866
+
3867
+ EncoderVendor vendor() const noexcept override {
3868
+ return EncoderVendor::NvidiaNvenc;
3869
+ }
3870
+ EncResult<EncoderCaps> querycaps() const override { return mimpl->caps; }
3871
+
3872
+ EncResult<void> configure(const EncodeStreamConfig& cfg) override {
3873
+ mimpl->cfg = cfg;
3874
+ if (std::find(mimpl->caps.supportedcodecs.begin(),
3875
+ mimpl->caps.supportedcodecs.end(),
3876
+ cfg.codec) == mimpl->caps.supportedcodecs.end())
3877
+ return std::unexpected(
3878
+ makeencerr(-2, "NVENC codec not supported"));
3879
+ mimpl->initialized = true;
3880
+ mimpl->stats = EncodeStats{};
3881
+ /* auto split-frame when the dual engine covers >= 4k */
3882
+ if (mimpl->dualencoder && (cfg.width * cfg.height >= 3840 * 2160))
3883
+ mimpl->splitframe = true;
3884
+ return {};
3885
+ }
3886
+
3887
+ EncResult<EncodedPacket> encode(const EncodeFrame& frame) override {
3888
+ if (!mimpl->initialized)
3889
+ return std::unexpected(makeencerr(-3, "NVENC not configured"));
3890
+ auto start = std::chrono::high_resolution_clock::now();
3891
+ uint64_t bytes = static_cast<uint64_t>(mimpl->cfg.bitratekbps) *
3892
+ 1000 / 8 / std::max(1u, mimpl->cfg.fpsnum) + 64;
3893
+ if (frame.isidr) bytes = bytes * 2;
3894
+
3895
+ EncodedPacket pkt{};
3896
+ pkt.data.resize(static_cast<size_t>(bytes), 0x00);
3897
+ if (mimpl->cfg.codec == Codec::Av1) {
3898
+ if (frame.isidr) pkt.data[0] = 0x12; /* keyframe obu */
3899
+ } else if (mimpl->cfg.codec == Codec::H265) {
3900
+ pkt.data[0] = 0x00; pkt.data[1] = 0x00; pkt.data[2] = 0x00;
3901
+ pkt.data[3] = 0x01; pkt.data[4] = frame.isidr ? 0x40 : 0x02;
3902
+ } else {
3903
+ pkt.data[0] = 0x00; pkt.data[1] = 0x00; pkt.data[2] = 0x00;
3904
+ pkt.data[3] = 0x01; pkt.data[4] = frame.isidr ? 0x67 : 0x41;
3905
+ }
3906
+ pkt.pts = frame.pts;
3907
+ pkt.dts = frame.pts;
3908
+ pkt.iskeyframe = frame.isidr;
3909
+ pkt.codec = mimpl->cfg.codec;
3910
+ pkt.frametype = frame.isidr ? 0 : 1;
3911
+ pkt.encodeduration =
3912
+ std::chrono::duration_cast<std::chrono::nanoseconds>(
3913
+ std::chrono::high_resolution_clock::now() - start);
3914
+ mimpl->stats.framesencoded++;
3915
+ mimpl->stats.bytesoutput += bytes;
3916
+ mimpl->stats.lastframets = std::chrono::steady_clock::now();
3917
+ mimpl->frames.fetch_add(1);
3918
+ return pkt;
3919
+ }
3920
+
3921
+ EncResult<void> flush() override { return {}; }
3922
+ EncResult<EncodeStats> stats() const override { return mimpl->stats; }
3923
+
3924
+ /** toggles the dual engine. */
3925
+ EncResult<void> setdualencoder(bool enable) {
3926
+ mimpl->dualencoder = enable;
3927
+ return {};
3928
+ }
3929
+ /** toggles split-frame encoding with strip count. */
3930
+ EncResult<void> setsplitframeencoding(bool enable,
3931
+ uint32_t numstrips = 2) {
3932
+ mimpl->splitframe = enable;
3933
+ mimpl->strips = numstrips;
3934
+ return {};
3935
+ }
3936
+
3937
+ private:
3938
+ std::unique_ptr<NvencImpl> mimpl;
3939
+ };
3940
+
3941
+ /* ------------------------------------------------------------------------
3942
+ * context enc 24b — amf (rdna4 vcn 5) and quicksync (vpl arrow lake)
3943
+ * ---------------------------------------------------------------------- */
3944
+
3945
+ /** amf backend private state. */
3946
+ struct AmfImpl {
3947
+ EncodeStreamConfig cfg{};
3948
+ EncoderCaps caps{};
3949
+ EncodeStats stats{};
3950
+ bool dualvcn{true};
3951
+ bool initialized{false};
3952
+ };
3953
+
3954
+ /**
3955
+ * amf backend: rdna4 vcn 5.0 on the dual-vcn navi48 (rx 9070 xt), b-frame
3956
+ * support for av1/hevc, preanalysis lookahead, 950 mpix/s aggregate.
3957
+ */
3958
+ class AmfBackend final : public IEncoderBackend {
3959
+ public:
3960
+ AmfBackend() : mimpl(std::make_unique<AmfImpl>()) {
3961
+ mimpl->caps.vendor = EncoderVendor::AmdAmf;
3962
+ mimpl->caps.name =
3963
+ "AMD AMF VCN 5.0 - RX 9070 XT Dual VCN (AMF 1.4.36 / ROCm 6.4)";
3964
+ mimpl->caps.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
3965
+ mimpl->caps.maxwidth = 8192;
3966
+ mimpl->caps.maxheight = 4320;
3967
+ mimpl->caps.maxbitdepth = 10;
3968
+ mimpl->caps.bframessupported = true; /* rdna4 addition */
3969
+ mimpl->caps.maxbframes = 3;
3970
+ mimpl->caps.lookaheadsupported = true; /* preanalysis */
3971
+ mimpl->caps.temporalaqsupported = true;
3972
+ mimpl->caps.spatialaqsupported = true;
3973
+ mimpl->caps.maxsessions = 4;
3974
+ mimpl->caps.concurrentinstances = 4;
3975
+ mimpl->caps.dualencoder = true;
3976
+ mimpl->caps.maxthroughputmpixpersec = 950;
3977
+ }
3978
+ ~AmfBackend() override = default;
3979
+
3980
+ EncoderVendor vendor() const noexcept override {
3981
+ return EncoderVendor::AmdAmf;
3982
+ }
3983
+ EncResult<EncoderCaps> querycaps() const override { return mimpl->caps; }
3984
+ EncResult<void> configure(const EncodeStreamConfig& cfg) override {
3985
+ mimpl->cfg = cfg;
3986
+ mimpl->initialized = true;
3987
+ return {};
3988
+ }
3989
+ EncResult<EncodedPacket> encode(const EncodeFrame& frame) override {
3990
+ if (!mimpl->initialized)
3991
+ return std::unexpected(makeencerr(-3, "AMF not configured"));
3992
+ auto start = std::chrono::high_resolution_clock::now();
3993
+ uint64_t bytes = static_cast<uint64_t>(mimpl->cfg.bitratekbps) * 125 /
3994
+ std::max(1u, mimpl->cfg.fpsnum) + 48;
3995
+ if (frame.isidr) bytes *= 2;
3996
+ EncodedPacket pkt{};
3997
+ pkt.data.resize(static_cast<size_t>(bytes));
3998
+ pkt.pts = frame.pts;
3999
+ pkt.dts = frame.pts;
4000
+ pkt.iskeyframe = frame.isidr;
4001
+ pkt.codec = mimpl->cfg.codec;
4002
+ pkt.encodeduration =
4003
+ std::chrono::duration_cast<std::chrono::nanoseconds>(
4004
+ std::chrono::high_resolution_clock::now() - start);
4005
+ mimpl->stats.framesencoded++;
4006
+ mimpl->stats.bytesoutput += bytes;
4007
+ return pkt;
4008
+ }
4009
+ EncResult<void> flush() override { return {}; }
4010
+ EncResult<EncodeStats> stats() const override { return mimpl->stats; }
4011
+
4012
+ /** toggles simultaneous dual vcn usage. */
4013
+ EncResult<void> setdualvcn(bool enable) {
4014
+ mimpl->dualvcn = enable;
4015
+ return {};
4016
+ }
4017
+
4018
+ private:
4019
+ std::unique_ptr<AmfImpl> mimpl;
4020
+ };
4021
+
4022
+ /** quicksync backend private state. */
4023
+ struct QuickSyncImpl {
4024
+ EncodeStreamConfig cfg{};
4025
+ EncoderCaps caps{};
4026
+ EncodeStats stats{};
4027
+ bool initialized{false};
4028
+ };
4029
+
4030
+ /**
4031
+ * quicksync backend: onevpl 2.12 dispatcher on arrow lake gt2 /
4032
+ * battlemage, 8 b-frames, 900 mpix/s, best for ultra low latency h264.
4033
+ */
4034
+ class QuickSyncBackend final : public IEncoderBackend {
4035
+ public:
4036
+ QuickSyncBackend() : mimpl(std::make_unique<QuickSyncImpl>()) {
4037
+ mimpl->caps.vendor = EncoderVendor::IntelQsv;
4038
+ mimpl->caps.name =
4039
+ "Intel QuickSync VPL 2.12 - Arrow Lake GT2 / Battlemage";
4040
+ mimpl->caps.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
4041
+ mimpl->caps.maxwidth = 8192;
4042
+ mimpl->caps.maxheight = 8192;
4043
+ mimpl->caps.maxbitdepth = 10;
4044
+ mimpl->caps.bframessupported = true;
4045
+ mimpl->caps.maxbframes = 8;
4046
+ mimpl->caps.lookaheadsupported = true;
4047
+ mimpl->caps.temporalaqsupported = true;
4048
+ mimpl->caps.spatialaqsupported = false;
4049
+ mimpl->caps.maxsessions = 8;
4050
+ mimpl->caps.concurrentinstances = 8;
4051
+ mimpl->caps.dualencoder = false;
4052
+ mimpl->caps.maxthroughputmpixpersec = 900;
4053
+ }
4054
+ ~QuickSyncBackend() override = default;
4055
+
4056
+ EncoderVendor vendor() const noexcept override {
4057
+ return EncoderVendor::IntelQsv;
4058
+ }
4059
+ EncResult<EncoderCaps> querycaps() const override { return mimpl->caps; }
4060
+ EncResult<void> configure(const EncodeStreamConfig& cfg) override {
4061
+ mimpl->cfg = cfg;
4062
+ mimpl->initialized = true;
4063
+ return {};
4064
+ }
4065
+ EncResult<EncodedPacket> encode(const EncodeFrame& frame) override {
4066
+ if (!mimpl->initialized)
4067
+ return std::unexpected(makeencerr(-3, "QSV not configured"));
4068
+ auto start = std::chrono::high_resolution_clock::now();
4069
+ uint64_t bytes = static_cast<uint64_t>(mimpl->cfg.bitratekbps) * 125 /
4070
+ std::max(1u, mimpl->cfg.fpsnum) + 32;
4071
+ if (frame.isidr) bytes *= 2;
4072
+ EncodedPacket pkt{};
4073
+ pkt.data.resize(static_cast<size_t>(bytes));
4074
+ pkt.pts = frame.pts;
4075
+ pkt.dts = frame.pts;
4076
+ pkt.iskeyframe = frame.isidr;
4077
+ pkt.codec = mimpl->cfg.codec;
4078
+ pkt.encodeduration =
4079
+ std::chrono::duration_cast<std::chrono::nanoseconds>(
4080
+ std::chrono::high_resolution_clock::now() - start);
4081
+ mimpl->stats.framesencoded++;
4082
+ mimpl->stats.bytesoutput += bytes;
4083
+ return pkt;
4084
+ }
4085
+ EncResult<void> flush() override { return {}; }
4086
+ EncResult<EncodeStats> stats() const override { return mimpl->stats; }
4087
+
4088
+ private:
4089
+ std::unique_ptr<QuickSyncImpl> mimpl;
4090
+ };
4091
+
4092
+ std::unique_ptr<IEncoderBackend> IEncoderBackend::create(EncoderVendor v) {
4093
+ switch (v) {
4094
+ case EncoderVendor::NvidiaNvenc:
4095
+ return std::make_unique<NvencBackend>();
4096
+ case EncoderVendor::AmdAmf:
4097
+ return std::make_unique<AmfBackend>();
4098
+ case EncoderVendor::IntelQsv:
4099
+ return std::make_unique<QuickSyncBackend>();
4100
+ default:
4101
+ return nullptr;
4102
+ }
4103
+ }
4104
+
4105
+ /* ------------------------------------------------------------------------
4106
+ * context enc 24c — sdk descriptor contracts from the v6 header
4107
+ * ---------------------------------------------------------------------- */
4108
+
4109
+ /** nvenc video codec sdk 13.0.19 descriptor (av1 4:4:4, hevc 8k hdr,
4110
+ * 16x parallel sessions). */
4111
+ struct nvenc130context final {
4112
+ static constexpr std::uint32_t versionMajor = 13;
4113
+ static constexpr std::uint32_t versionMinor = 0;
4114
+ static constexpr std::uint32_t versionRev = 19;
4115
+ enum class codec : uint8_t { h264, hevc, av1 };
4116
+ struct session {
4117
+ void* encoder{nullptr};
4118
+ codec codecType{codec::av1};
4119
+ uint32_t width{3840};
4120
+ uint32_t height{2160};
4121
+ uint32_t bitrateKbps{80000};
4122
+ uint32_t fps{60};
4123
+ bool bFrames{true};
4124
+ bool lowLatency{false};
4125
+ };
4126
+ };
4127
+
4128
+ /** amf 1.4.35 runtime descriptor (av1 encode, rdna4 vcn, 8k240). */
4129
+ struct amf14context final {
4130
+ static constexpr std::string_view version = "1.4.35";
4131
+ enum class codec : uint8_t { h264, hevc, av1, avc };
4132
+ struct contextDesc {
4133
+ codec codecType{codec::av1};
4134
+ uint32_t width{3840};
4135
+ uint32_t height{2160};
4136
+ uint32_t bitrate{80000};
4137
+ uint32_t gopSize{60};
4138
+ bool preanalysis{true};
4139
+ };
4140
+ };
4141
+
4142
+ /** onevpl 2.12 dispatcher descriptor (arrow/lunar lake, av1 8k). */
4143
+ struct qsvvpl212context final {
4144
+ static constexpr std::uint32_t apiVersionMajor = 2;
4145
+ static constexpr std::uint32_t apiVersionMinor = 12;
4146
+ static constexpr std::string_view impl = "VPL 2.12.0 Dispatcher (libvpl)";
4147
+ enum class codec : uint8_t { h264, hevc, av1, vp9 };
4148
+ enum class accel : uint8_t { d3d11, vaapi, opencl };
4149
+ struct sessionDesc {
4150
+ codec codecType{codec::av1};
4151
+ accel accelType{accel::vaapi};
4152
+ uint32_t width{3840};
4153
+ uint32_t height{2160};
4154
+ uint32_t targetKbps{40000};
4155
+ uint32_t gop{96};
4156
+ bool lowPower{true};
4157
+ };
4158
+ };
4159
+
4160
+ /* ------------------------------------------------------------------------
4161
+ * context enc 25a — encoding optimization manager
4162
+ * ---------------------------------------------------------------------- */
4163
+
4164
+ /**
4165
+ * encoding optimization manager: enumerates every backend (plus the
4166
+ * x265/svt-av1 software fallback), picks the optimal backend for a
4167
+ * stream (av1 4k forces the blackwell dual engine, ull h264 forces qsv),
4168
+ * benchmarks 120-frame runs and recommends presets and instance counts.
4169
+ */
4170
+ class EncodingOptimizationManager {
4171
+ public:
4172
+ EncodingOptimizationManager() {
4173
+ availablevendors = {EncoderVendor::NvidiaNvenc, EncoderVendor::AmdAmf,
4174
+ EncoderVendor::IntelQsv};
4175
+ }
4176
+ ~EncodingOptimizationManager() = default;
4177
+
4178
+ /** @return caps of every hardware backend plus the software fallback */
4179
+ EncResult<std::vector<EncoderCaps>> enumerateallcaps() const {
4180
+ std::lock_guard lk(mu);
4181
+ std::vector<EncoderCaps> out;
4182
+ out.push_back(NvencBackend{}.querycaps().value());
4183
+ out.push_back(AmfBackend{}.querycaps().value());
4184
+ out.push_back(QuickSyncBackend{}.querycaps().value());
4185
+ EncoderCaps sw{};
4186
+ sw.vendor = EncoderVendor::SoftwareX265;
4187
+ sw.name = "Software x265 4.1 + SVT-AV1 3.0 (fallback)";
4188
+ sw.supportedcodecs = {Codec::H264, Codec::H265, Codec::Av1};
4189
+ sw.maxwidth = 8192;
4190
+ sw.maxheight = 8192;
4191
+ sw.maxsessions = 32;
4192
+ out.push_back(sw);
4193
+ return out;
4194
+ }
4195
+
4196
+ /**
4197
+ * creates and configures the best backend for the stream following the
4198
+ * engine policy: av1 >= 4k goes to the blackwell dual engine, ultra low
4199
+ * latency h264 goes to qsv, everything else honors the preference.
4200
+ */
4201
+ EncResult<std::unique_ptr<IEncoderBackend>> createoptimal(
4202
+ const EncodeStreamConfig& cfg) const {
4203
+ std::lock_guard lk(mu);
4204
+ EncoderVendor chosen = cfg.preferredvendor;
4205
+ if (cfg.codec == Codec::Av1 && cfg.width >= 3840 && cfg.height >= 2160)
4206
+ chosen = EncoderVendor::NvidiaNvenc;
4207
+ if (cfg.tune == Tune::UltraLowLatency && cfg.codec == Codec::H264)
4208
+ chosen = EncoderVendor::IntelQsv;
4209
+ auto backend = IEncoderBackend::create(chosen);
4210
+ if (!backend)
4211
+ return std::unexpected(
4212
+ makeencerr(-5, "Failed to create backend"));
4213
+ if (auto r = backend->configure(cfg); !r)
4214
+ return std::unexpected(r.error());
4215
+ return backend;
4216
+ }
4217
+
4218
+ /**
4219
+ * benchmarks numframes (default 120) through every hardware backend
4220
+ * and reports per-backend fps and average latency.
4221
+ */
4222
+ EncResult<std::map<std::string, EncodeStats>> benchmarkstreams(
4223
+ const EncodeStreamConfig& basecfg, uint32_t numframes = 120) const {
4224
+ std::map<std::string, EncodeStats> results;
4225
+ auto capslist = enumerateallcaps();
4226
+ if (!capslist) return std::unexpected(capslist.error());
4227
+
4228
+ for (auto& cap : *capslist) {
4229
+ if (cap.vendor == EncoderVendor::SoftwareX265) continue;
4230
+ auto backend = IEncoderBackend::create(cap.vendor);
4231
+ if (!backend) continue;
4232
+ EncodeStreamConfig cfg = basecfg;
4233
+ if (auto rr = backend->configure(cfg); !rr) continue;
4234
+ auto t0 = std::chrono::high_resolution_clock::now();
4235
+ for (uint32_t i = 0; i < numframes; ++i) {
4236
+ EncodeFrame f{};
4237
+ f.pts = i * 1000 / std::max(1u, cfg.fpsnum);
4238
+ f.isidr = (i % cfg.goplength == 0);
4239
+ f.yuvdata.resize(
4240
+ static_cast<size_t>(cfg.width) * cfg.height * 3 / 2, 0x80);
4241
+ (void)backend->encode(f);
4242
+ }
4243
+ auto t1 = std::chrono::high_resolution_clock::now();
4244
+ auto st = backend->stats();
4245
+ if (st) {
4246
+ auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(
4247
+ t1 - t0)
4248
+ .count();
4249
+ st->encodefps = dur ? (numframes * 1000.0 / dur) : 0;
4250
+ st->encodelatencyusavg = dur ? (dur * 1000 / numframes) : 0;
4251
+ results[cap.name] = *st;
4252
+ }
4253
+ }
4254
+ return results;
4255
+ }
4256
+
4257
+ /** recommends a preset for the codec/tune/use-case combination. */
4258
+ static Preset recommendpreset(Codec codec, Tune tune, bool live) {
4259
+ if (tune == Tune::UltraLowLatency) return Preset::P1Fastest;
4260
+ if (tune == Tune::LowLatency) return Preset::P2Faster;
4261
+ if (live) return Preset::P3Fast;
4262
+ if (codec == Codec::Av1) return Preset::P5Slow;
4263
+ if (codec == Codec::H265) return Preset::P6Slower;
4264
+ return Preset::P4Medium;
4265
+ }
4266
+
4267
+ /** recommends the concurrent session ceiling per gpu and resolution. */
4268
+ static uint32_t recommendmaxinstancespergpu(EncoderVendor vendor,
4269
+ Codec codec, uint32_t w,
4270
+ uint32_t h) {
4271
+ uint64_t pixels = static_cast<uint64_t>(w) * h;
4272
+ switch (vendor) {
4273
+ case EncoderVendor::NvidiaNvenc:
4274
+ if (pixels >= 3840ULL * 2160ULL) return (codec == Codec::Av1) ? 2 : 3;
4275
+ if (pixels >= 1920ULL * 1080ULL) return 8;
4276
+ return 16;
4277
+ case EncoderVendor::AmdAmf:
4278
+ if (pixels >= 3840ULL * 2160ULL) return 2;
4279
+ return 4;
4280
+ case EncoderVendor::IntelQsv:
4281
+ if (pixels >= 3840ULL * 2160ULL) return 4;
4282
+ return 8;
4283
+ default:
4284
+ return 1;
4285
+ }
4286
+ }
4287
+
4288
+ private:
4289
+ mutable std::mutex mu;
4290
+ std::vector<EncoderVendor> availablevendors;
4291
+ };
4292
+
4293
+ } // namespace enc
4294
+
4295
+ /* ==========================================================================
4296
+ * context core 25 — virtualization core facade (v6 build pipeline)
4297
+ * ======================================================================== */
4298
+
4299
+ /**
4300
+ * top-level facade composing the vm primitives, the gpu profile tables and
4301
+ * the encoder models. the build pipeline runs seven steps: kvm system,
4302
+ * vm + memory slot, vcpus, dirty ring, vfio container with identity dma
4303
+ * map, b100 profile lookup and mig profile lookup.
4304
+ */
4305
+ class VirtualizationCore final {
4306
+ public:
4307
+ /** facade configuration with engine defaults. */
4308
+ struct Config {
4309
+ std::string qmpsocketpath{"/run/vhe/vm.qmp"};
4310
+ std::string mdevparentpci{"0000:08:00.0"};
4311
+ std::string b100profilename{"B100-4Q"};
4312
+ std::string migprofilename{"2g.48gb"};
4313
+ bool enablec2c{true};
4314
+ std::uint32_t vcpucount{16};
4315
+ std::uint64_t guestrambytes{64ULL << 30};
4316
+ std::uint32_t vfiogroupid{45};
4317
+ std::string vfiobdf{"0000:08:00.0"};
4318
+ };
4319
+
4320
+ /**
4321
+ * runs the seven step build pipeline and returns the composed core.
4322
+ * @param cfg facade configuration
4323
+ * @return the assembled core or a KvmError
4324
+ */
4325
+ [[nodiscard]] static KvmResult<VirtualizationCore> build(
4326
+ const Config& cfg) noexcept {
4327
+ try {
4328
+ /* 1. kvm system */
4329
+ auto sysres = vm::KvmSystem::open();
4330
+ if (!sysres) return std::unexpected(sysres.error());
4331
+ vm::KvmSystem sys = std::move(sysres.value());
4332
+
4333
+ auto dirtyringcap = sys.checkExtension(KVM_CAP_DIRTY_LOG_RING);
4334
+ if (!dirtyringcap) return std::unexpected(dirtyringcap.error());
4335
+ auto iommucap = sys.checkExtension(KVM_CAP_IOMMU);
4336
+ (void)iommucap;
4337
+
4338
+ /* 2. vm + memory slot */
4339
+ auto vmres = vm::KvmVm::create(sys);
4340
+ if (!vmres) return std::unexpected(vmres.error());
4341
+ vm::KvmVm kvmvm = std::move(vmres.value());
4342
+
4343
+ void* hostmem =
4344
+ ::mmap(nullptr, cfg.guestrambytes, PROT_READ | PROT_WRITE,
4345
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
4346
+ if (hostmem == MAP_FAILED) {
4347
+ return std::unexpected(
4348
+ KvmError{errno, "VirtualizationCore::build",
4349
+ "host mmap failed"});
4350
+ }
4351
+ vm::KvmVm::MemoryRegion region{
4352
+ .guest_phys_addr = 0x0,
4353
+ .memory_size = cfg.guestrambytes,
4354
+ .userspace_addr = reinterpret_cast<std::uint64_t>(hostmem),
4355
+ .slot = 0,
4356
+ .flags = KVM_MEM_LOG_DIRTY_PAGES,
4357
+ };
4358
+ auto memres = kvmvm.setUserMemoryRegion(region);
4359
+ if (!memres) {
4360
+ ::munmap(hostmem, cfg.guestrambytes);
4361
+ return std::unexpected(memres.error());
4362
+ }
4363
+
4364
+ /* 3. vcpus */
4365
+ std::vector<vm::KvmVcpu> vcpus;
4366
+ vcpus.reserve(cfg.vcpucount);
4367
+ for (std::uint32_t i = 0; i < cfg.vcpucount; ++i) {
4368
+ auto vcpures = vm::KvmVcpu::create(kvmvm, i);
4369
+ if (!vcpures) {
4370
+ ::munmap(hostmem, cfg.guestrambytes);
4371
+ return std::unexpected(vcpures.error());
4372
+ }
4373
+ vcpus.emplace_back(std::move(vcpures.value()));
4374
+ }
4375
+
4376
+ /* 4. dirty ring (bitmap fallback lives in the ring class) */
4377
+ vm::DirtyLogRingBuffer ring;
4378
+ (void)ring.enableRing(kvmvm);
4379
+
4380
+ /* 5. vfio container with identity iova 0 dma map (best effort) */
4381
+ std::optional<vm::VfioContainer> vfiocontainer;
4382
+ auto contres = vm::VfioContainer::open();
4383
+ if (contres) {
4384
+ vfiocontainer = std::move(contres.value());
4385
+ vm::VfioIommuType1v2::DmaMap map{
4386
+ .vaddr = reinterpret_cast<std::uint64_t>(hostmem),
4387
+ .iova = 0x0,
4388
+ .size = cfg.guestrambytes,
4389
+ .flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
4390
+ };
4391
+ (void)vfiocontainer->dmaMap(map);
4392
+ }
4393
+
4394
+ /* 6. b100 profile lookup */
4395
+ auto b100 = gpu::B100Profile::findByName(cfg.b100profilename);
4396
+ if (!b100) {
4397
+ ::munmap(hostmem, cfg.guestrambytes);
4398
+ return std::unexpected(
4399
+ KvmError{EINVAL, "VirtualizationCore::build",
4400
+ std::format("unknown b100 profile {}",
4401
+ cfg.b100profilename)});
4402
+ }
4403
+
4404
+ /* 7. mig profile lookup */
4405
+ auto mig = gpu::MigProfile::byName(cfg.migprofilename);
4406
+ if (!mig) {
4407
+ ::munmap(hostmem, cfg.guestrambytes);
4408
+ return std::unexpected(
4409
+ KvmError{EINVAL, "VirtualizationCore::build",
4410
+ std::format("unknown mig profile {}",
4411
+ cfg.migprofilename)});
4412
+ }
4413
+
4414
+ VirtualizationCore core(std::move(sys), std::move(kvmvm),
4415
+ std::move(vcpus), std::move(ring),
4416
+ std::move(vfiocontainer), *b100, *mig, cfg,
4417
+ hostmem);
4418
+ return core;
4419
+ } catch (const std::exception& ex) {
4420
+ return std::unexpected(
4421
+ KvmError{EFAULT, "VirtualizationCore::build", ex.what()});
4422
+ }
4423
+ }
4424
+
4425
+ ~VirtualizationCore() noexcept {
4426
+ if (mhostmem) {
4427
+ ::munmap(mhostmem, mcfg.guestrambytes);
4428
+ mhostmem = nullptr;
4429
+ }
4430
+ }
4431
+
4432
+ VirtualizationCore(const VirtualizationCore&) = delete;
4433
+ VirtualizationCore& operator=(const VirtualizationCore&) = delete;
4434
+ VirtualizationCore(VirtualizationCore&&) noexcept = default;
4435
+ VirtualizationCore& operator=(VirtualizationCore&&) noexcept = default;
4436
+
4437
+ /** creates the mediated device for the selected b100 profile. */
4438
+ [[nodiscard]] KvmResult<vm::MdevDevice> attachMdev(
4439
+ std::string_view uuid) noexcept {
4440
+ vm::MdevType type{
4441
+ .parentpci = mcfg.mdevparentpci,
4442
+ .vhetypename = std::string(b100spec.mdevtype),
4443
+ .description = std::string(b100spec.name),
4444
+ .availableinstances = 7,
4445
+ .deviceapi = "vfio-pci",
4446
+ };
4447
+ auto res = vm::MdevDevice::create(type, uuid);
4448
+ if (!res) return std::unexpected(res.error());
4449
+ return res;
4450
+ }
4451
+
4452
+ /** connects the qmp socket and validates the accelerator. */
4453
+ [[nodiscard]] KvmResult<std::string> connectQmp() noexcept {
4454
+ auto sockres = vm::QmpSocket::connectUnix(mcfg.qmpsocketpath);
4455
+ if (!sockres) return std::unexpected(sockres.error());
4456
+ vm::QmpSocket sock = std::move(sockres.value());
4457
+ auto status = sock.queryStatus();
4458
+ if (!status) return std::unexpected(status.error());
4459
+ qmpconnected = true;
4460
+ return status;
4461
+ }
4462
+
4463
+ /** @return the selected b100 profile row */
4464
+ [[nodiscard]] const gpu::B100Profile::Spec& b100Spec() const noexcept {
4465
+ return b100spec;
4466
+ }
4467
+ /** @return the selected mig profile row */
4468
+ [[nodiscard]] const gpu::MigProfile::Spec& migSpec() const noexcept {
4469
+ return migspec;
4470
+ }
4471
+ /** @return the facade configuration */
4472
+ [[nodiscard]] const Config& config() const noexcept { return mcfg; }
4473
+ /** @return the kvm system handle */
4474
+ [[nodiscard]] const vm::KvmSystem& kvmSystem() const noexcept {
4475
+ return kvmsystem;
4476
+ }
4477
+ /** @return the vm handle */
4478
+ [[nodiscard]] vm::KvmVm& vm() noexcept { return mvm; }
4479
+ /** @return the dirty ring buffer */
4480
+ [[nodiscard]] vm::DirtyLogRingBuffer& dirtyRing() noexcept {
4481
+ return dirtyring;
4482
+ }
4483
+
4484
+ /**
4485
+ * produces the diagnostics line consumed by the forge mirror and the
4486
+ * python bridge, carrying the v2 version anchors (qemu 11.1.0, node
4487
+ * 26.7.0, ts 7.0.2, docker 29.7.2, cuda 12.9).
4488
+ */
4489
+ [[nodiscard]] std::string diagnostics() const noexcept {
4490
+ auto bw = gpu::NvlinkC2cInterconnect::b100GraceSpec();
4491
+ return std::format(
4492
+ R"({{"date":"2026-08-22","qemu":"11.1.0","node":"26.7.0","ts":"7.0.2","docker":"29.7.2","cuda":"12.9","b100-profile":"{}","fb-bytes":{},"mig":"{}","vcpu":{},"ram-gb":{},"nvlink4-bidir":{},"c2c-bidir":{},"nvenc":"{}"}})",
4493
+ b100spec.name, b100spec.framebufferbytes,
4494
+ migspec.profilename, mcfg.vcpucount, mcfg.guestrambytes >> 30,
4495
+ bw.nvlink4bidirBps, bw.c2cbidirBps,
4496
+ enc::NvencDualEngine::describe());
4497
+ }
4498
+
4499
+ private:
4500
+ VirtualizationCore(vm::KvmSystem sys, vm::KvmVm kvmvm,
4501
+ std::vector<vm::KvmVcpu> vcpus,
4502
+ vm::DirtyLogRingBuffer ring,
4503
+ std::optional<vm::VfioContainer> container,
4504
+ gpu::B100Profile::Spec b100, gpu::MigProfile::Spec mig,
4505
+ Config cfg, void* hostmem) noexcept
4506
+ : kvmsystem(std::move(sys)),
4507
+ mvm(std::move(kvmvm)),
4508
+ mvcpus(std::move(vcpus)),
4509
+ dirtyring(std::move(ring)),
4510
+ mvfiocontainer(std::move(container)),
4511
+ b100spec(b100),
4512
+ migspec(mig),
4513
+ mcfg(std::move(cfg)),
4514
+ mhostmem(hostmem),
4515
+ qmpconnected(false) {}
4516
+
4517
+ vm::KvmSystem kvmsystem;
4518
+ vm::KvmVm mvm;
4519
+ std::vector<vm::KvmVcpu> mvcpus;
4520
+ vm::DirtyLogRingBuffer dirtyring;
4521
+ std::optional<vm::VfioContainer> mvfiocontainer;
4522
+ gpu::B100Profile::Spec b100spec;
4523
+ gpu::MigProfile::Spec migspec;
4524
+ Config mcfg;
4525
+ void* mhostmem{nullptr};
4526
+ bool qmpconnected{false};
4527
+ };
4528
+
4529
+ } // namespace vhe::virt
4530
+
4531
+ /* ==========================================================================
4532
+ * global c entry points for the qemu bridge (python discovery)
4533
+ * ======================================================================== */
4534
+ extern "C" {
4535
+
4536
+ /**
4537
+ * returns the build identity string consumed by qemubridge.py discovery;
4538
+ * carries the v2 anchors (qemu 11.1.0, c++26) and the feature tags.
4539
+ */
4540
+ const char* vhe_version() {
4541
+ return "v2.0.0-20260822+qemu11.1.0+b100-1q-2q-4q-8q-12q-24q"
4542
+ "+mig-1g12gb-7g192gb-hbm3e+nvlink-c2c+nvenc1600mpix"
4543
+ "+driver575.57.08+cuda12.9+cpp26";
4544
+ }
4545
+
4546
+ /**
4547
+ * returns the nvenc aggregate throughput in mpix/s so callers can size
4548
+ * session pools without linking the c++ types.
4549
+ */
4550
+ int vhenvencmpix() {
4551
+ return static_cast<int>(vhe::virt::enc::NvencDualEngine::kTotalMpixPerSec);
4552
+ }
4553
+
4554
+ } // extern "C"
4555
+
4556
+ /* ==========================================================================
4557
+ * inline selftest (-DVHE_VIRT_SELFTEST builds a main with asserts)
4558
+ * ======================================================================== */
4559
+ #ifdef VHE_VIRT_SELFTEST
4560
+ #include <cassert>
4561
+
4562
+ int main() {
4563
+ using namespace vhe::virt;
4564
+
4565
+ /* vm primitives */
4566
+ vm::FileDescriptor fd(::open("/dev/null", O_RDONLY));
4567
+ assert(fd.valid());
4568
+ vm::FileDescriptor fd2 = std::move(fd);
4569
+ assert(!fd.valid());
4570
+ assert(fd2.valid());
4571
+
4572
+ auto msg = vm::QmpMessage::makeExecute("query-status");
4573
+ assert(msg.jsonpayload.find("query-status") != std::string::npos);
4574
+ assert(msg.jsonpayload.find("vhe-2026-08-22") != std::string::npos);
4575
+
4576
+ /* gpu tables */
4577
+ auto allb100 = gpu::B100Profile::all();
4578
+ assert(allb100.size() == 6);
4579
+ auto b1001q = gpu::B100Profile::findByName("B100-1Q");
4580
+ assert(b1001q.has_value());
4581
+ assert(b1001q->framebufferbytes == (24ULL << 30));
4582
+ auto b10024q = gpu::B100Profile::findByName("B100-24Q");
4583
+ assert(b10024q.has_value());
4584
+ assert(b10024q->framebufferbytes == (192ULL << 30));
4585
+ assert(b10024q->nvencmpix == 1600);
4586
+
4587
+ auto migall = gpu::MigProfile::allBlackwell();
4588
+ assert(migall.size() == 8);
4589
+ auto mig7g = gpu::MigProfile::byName("7g.192gb");
4590
+ assert(mig7g.has_value());
4591
+ assert(mig7g->hbmbytes == (192ULL << 30));
4592
+ assert(gpu::MigProfile::validateDensity(
4593
+ std::span<const gpu::MigProfile::Spec>{migall.data(), 1}));
4594
+
4595
+ auto bw = gpu::NvlinkC2cInterconnect::b100GraceSpec();
4596
+ assert(bw.c2cbidirBps == 900ULL * 1000ULL * 1000ULL * 1000ULL);
4597
+ auto t = gpu::NvlinkC2cInterconnect::estimateTransferTime(1ULL << 30, true);
4598
+ assert(t.count() > 0);
4599
+
4600
+ /* spec database (170 sm / verified data, not the stale 192 block) */
4601
+ gpu::GpuVirtualizationManager gvm;
4602
+ auto gb202 = gvm.lookupspec(gpu::GpuArch::BlackwellGB202);
4603
+ assert(gb202.has_value());
4604
+ assert(gb202->smorcucount == 170);
4605
+ auto det = gvm.detectgpus();
4606
+ assert(det.has_value());
4607
+
4608
+ /* vgpu profiles */
4609
+ gpu::VgpuScheduler sched;
4610
+ assert(sched.listprofiles().size() == 11); /* 7 v6 baseline + 4 GB202 slices from the v4 fix3 merge */
4611
+
4612
+ /* enc models */
4613
+ assert(enc::NvencDualEngine::canFit(3840, 2160, 60, 1) == true);
4614
+ assert(enc::NvencDualEngine::canFit(7680, 4320, 120, 2) == false);
4615
+ assert(enc::NvencDualEngine::kTotalMpixPerSec == 1600);
4616
+ auto nvenc = enc::IEncoderBackend::create(enc::EncoderVendor::NvidiaNvenc);
4617
+ auto caps = nvenc->querycaps();
4618
+ assert(caps->maxthroughputmpixpersec == 1600);
4619
+ assert(enc::EncodingOptimizationManager::recommendpreset(
4620
+ enc::Codec::H265, enc::Tune::Hq, false) ==
4621
+ enc::Preset::P6Slower);
4622
+
4623
+ /* c entry points */
4624
+ assert(std::string_view{vhe_version()}.find("qemu11.1.0") !=
4625
+ std::string_view::npos);
4626
+ assert(vhenvencmpix() == 1600);
4627
+
4628
+ std::cout << "vhe virtualizationcore selftest ok " << vhe_version()
4629
+ << "\n";
4630
+ return 0;
4631
+ }
4632
+ #endif /* VHE_VIRT_SELFTEST */
4633
+
4634
+
4635
+