Audience: Graphics subsystem authors, DirectX-translation contributors
Execution context: Kernel — currently CPU-side only; no real GPU submission in v1
Maturity: v1 — instance / device / queue / command-buffer lifecycle
- clear-to-scanout + CPU triangle rasterizer + SPIR-V interpreter (parser + executor for the canonical Vulkan subset; wired into
vkCmdDrawvia the shader-rasterizer hook when a pipeline binds parseable VS + FS modules)
DuetOS hosts its Vulkan ICD inside the kernel. The reason is pragmatic: the kernel already owns the GPU command rings (NVMe-style PCIe submission queues, virtio-gpu virtqueues, the Intel iGPU execlist), and the Subsystem Isolation rule says every effect a guest binary can have on the system goes through a kernel-mediated cap-gated path. Putting the ICD in the kernel means Vulkan calls funnel into the same place as every other GPU-touching code path.
The v0 ICD is CPU-side only. Command buffers record opcodes into a
per-buffer tape. vkQueueSubmit replays the tape on the CPU; the only
opcodes that produce visible output are vkCmdClearColorImage (against
a scanout-backed image) and vkQueuePresentKHR (which flushes the
damage rect through the compositor).
What that buys us today:
- The Vulkan handle-lifecycle is exercised end-to-end — every PE that
pulls in
vulkan-1.dll(and there are several already) loads cleanly. - The DirectX 11 / 12 translators (DirectX) bind to a real device + queue + command list shape.
- The WSI surface acquires + presents real framebuffer pixels.
What it does not buy us yet: pixel-shader execution, vertex processing, or any submission to real silicon. Those are the gating slices for the per-vendor GPU drivers (Graphics Drivers) to actually paint their commands.
| File | Purpose |
|---|---|
graphics.h / .cpp |
Subsystem entry, public stats |
graphics_vk.cpp |
Instance / device / queue lifecycle |
graphics_vk_commands.cpp |
Command buffer tape (record + replay) |
graphics_vk_descriptors.cpp |
Descriptor sets, pools, layouts |
graphics_vk_misc.cpp |
Format conversions, image creation, memory mapping |
graphics_vk_wsi.cpp |
WSI — surface, swapchain, present |
graphics_vk_raster.cpp |
Fixed-function CPU triangle rasterizer (v0/v1 vertex format) |
graphics_vk_depth.cpp |
Software 16-bit depth surface |
graphics_vk_spirv.h |
SPIR-V interpreter public interface |
graphics_vk_spirv_parse.cpp |
Module parser (types, constants, decorations, basic blocks) |
graphics_vk_spirv_exec.cpp |
Interpreter execution engine |
graphics_vk_spirv_selftest.cpp |
Boot self-test (3 canonical SPIR-V modules) |
graphics_vk_shaderraster.cpp |
Shader-based rasterizer hook (vkCmdDraw -> SPIR-V interpreter) |
graphics_vk_selftest.cpp |
Boot self-test (lifecycle) |
graphics_vk_internal.h |
Shared per-handle structures |
Every Vulkan handle (VkInstance, VkDevice, VkQueue,
VkCommandBuffer, VkImage, VkBuffer, VkPipeline, …) is a
slot-indexed 64-bit ID. Per-kind pools live in the subsystem with
fixed slot counts (32 per kind in v0). The ID is composed
base + slot where the base differs per kind (e.g. instance handles
start at 0x10000, devices at 0x20000, …). That makes a stray
"wrong kind of handle" obvious in a debugger without paying the cost
of a real opaque dispatch table.
Dispatch is direct C++ function calls — no vendor ICD trampolines,
no vk_layer_dispatch_table. Userland calls into vulkan-1.dll,
which thunks to the subsystem entry points via the standard Win32
syscall path.
vkBeginCommandBuffer allocates a fresh tape; every vkCmd* call
appends an opcode + parameters. vkEndCommandBuffer flips the buffer
state from "recording" to "executable."
vkQueueSubmit walks the tape. Today's opcodes:
| Opcode | What it does in v0 |
|---|---|
vkCmdClearColorImage |
Writes the clear colour to the image's backing memory. If the image is scanout-backed, the framebuffer's damage rect is widened. |
vkCmdBindPipeline, vkCmdBindDescriptorSets, vkCmdBindIndexBuffer |
Update tape-local "current state" only. |
vkCmdBindVertexBuffers |
Recorded; the binding-0 buffer + offset feed the software triangle rasterizer at vkCmdDraw replay time. |
vkCmdDraw |
Software triangle rasterizer — paints flat-shaded triangles into the bound scanout-backed render target via the DuetOS v0 fixed vertex format (see below). Bumps vk_triangles_drawn by vertex_count / 3 regardless of whether pixels reach the framebuffer. |
vkCmdDrawIndexed, vkCmdDispatch |
Bump a counter. No pixels move (no index-buffer fetch in the rasterizer yet; no shader to run for compute). |
vkCmdCopyBuffer, vkCmdCopyImage, vkCmdCopyBufferToImage |
Real memcpy between mapped backing buffers. |
vkCmdPipelineBarrier, vkCmdSetViewport, vkCmdSetScissor |
No-op (state recorded, not enforced). |
graphics_vk_raster.cpp is a CPU edge-function rasterizer that
turns vkCmdDraw and vkCmdDrawIndexed into visible pixels when
the caller fills its vertex buffer with one of the DuetOS fixed
vertex formats and points the draw at a scanout-backed render
target. No SPIR-V execution, no vertex transform — positions are
already in pixel space — but the rasterizer interpolates colour
(Gouraud), honours per-pixel alpha (src-over) and runs a
hardware-style depth test on the v1 vertex format.
Vertex formats:
| Format | Stride | Layout |
|---|---|---|
| v0 (default) | 8 bytes | {i16 x_px; i16 y_px; u32 argb;} |
| v1 (with depth) | 12 bytes | {i16 x_px; i16 y_px; i16 z; u16 reserved; u32 argb;} |
The format is per-command-buffer state. Defaults to v0;
callers select v1 via the DuetOS extension
vkCmdSetVertexFormatDuet(cb, 1). argb is 0xAARRGGBB; the
high byte drives FramebufferBlendPixel (src-over blend)
when < 0xFF and FramebufferPutPixel (opaque) when == 0xFF.
Topologies (Vulkan spec values):
| Topology | Value | Primitives produced |
|---|---|---|
| PointList | 0 | vertex_count 1×1 pixel stamps |
| LineList | 1 | vertex_count / 2 Bresenham segments |
| LineStrip | 2 | vertex_count - 1 Bresenham segments |
| TriangleList | 3 | vertex_count / 3 triangles |
| TriangleStrip | 4 | vertex_count - 2 triangles (odd triangles flip winding) |
| TriangleFan | 5 | vertex_count - 2 triangles (every triangle shares vertex 0) |
Point and line topologies are flat-shaded with the first vertex's
colour and bypass the triangle bbox walk. *_with_adjacency
topologies record but produce no pixels. Selected via
vkCmdSetPrimitiveTopology(cb, n); defaults to TriangleList.
Indexed draws: vkCmdDrawIndexed walks the buffer bound by
vkCmdBindIndexBuffer. UINT16 and UINT32 index formats are
both supported; each index is offset by the draw's
vertex_offset parameter before lookup in the vertex buffer.
Strip and fan topologies are honoured the same way as the
non-indexed path.
Scissor: the rasterizer's bounding-box walk is intersected
with the most-recent vkCmdSetScissor rect when it's
non-empty. A zero-extent scissor disables enforcement.
Depth test: when the vertex format is v1 AND
vkCmdSetDepthTestEnable(cb, 1) has run AND the shared depth
surface allocates successfully:
- Z is interpolated barycentrically per pixel using the same weights as colour.
- The depth surface is a single
u16per-pixel buffer sized to the live framebuffer extent (one buffer total — v0 doesn't multi-target). Lazy-allocated throughkheapon the first Z-test draw; cleared to0xFFFF(far) at alloc and byvkCmdClearDepthStencilImage(recognising the canonical0.0f/1.0fbit patterns to avoid pulling in soft-float). vkCmdSetDepthCompareOp(cb, op)honours every Vulkan compare op (Never / Less / Equal / LessOrEqual / Greater / NotEqual / GreaterOrEqual / Always). Default Less.vkCmdSetDepthWriteEnable(cb, 1)gates the write-back of the new Z value to the depth surface; the compare still runs when write is disabled.
When the depth surface can't allocate (low memory, headless boot) the rasterizer silently falls back to the no-Z path and logs one WARN line — no per-frame chatter.
Algorithm: integer edge-function (barycentric) test. For
each pixel (px, py) in the clipped bounding box, compute the
three edge functions; a pixel is inside when all three share a
sign consistent with the triangle's signed area. Barycentric
weights are the absolute edge magnitudes; channels and Z are
interpolated as
(w0*a + w1*b + w2*c + |area|/2) / |area|. Degenerate
(zero-area) triangles are skipped. The flat-shade fast path
(all three vertex colours identical) skips the per-pixel
divide.
Replay state machine — these per-cb commands feed the rasterizer at submit time:
| Command | Effect on RasterState |
|---|---|
vkCmdBeginRenderPass / BeginRendering / ClearColorImage |
sets rt_image |
vkCmdBindVertexBuffers(binding=0) |
sets vertex_buffer + vertex_offset |
vkCmdBindIndexBuffer |
sets index_buffer + index_offset + index_type |
vkCmdSetScissor |
sets scissor + has_scissor (cleared by zero-extent rect) |
vkCmdSetPrimitiveTopology |
sets topology |
vkCmdSetVertexFormatDuet (extension) |
sets vertex_format (0 = v0, 1 = v1) |
vkCmdSetDepthTestEnable / SetDepthWriteEnable / SetDepthCompareOp |
gates depth |
vkCmdClearDepthStencilImage |
lazy-allocates + clears the shared depth surface |
State is per-command-buffer; a secondary cb invoked via
vkCmdExecuteCommands starts with fresh state in its own
recursion of the replay walker.
Counters — vk_triangles_drawn ticks per dispatched
triangle (TriangleList: vertex_count / 3; strip/fan:
vertex_count - 2; same for indexed-draw counts) regardless of
whether the rasterizer actually paints (counter bumps before
the scanout / host-visible / format gates), so the dispatch
chain is observable to tests that don't own the live
framebuffer.
Front-face culling: vkCmdSetCullMode(cb, mode) and
vkCmdSetFrontFace(cb, face) enforce backface / frontface
culling at raster time. Cull modes: 0=None, 1=Front, 2=Back,
3=FrontAndBack. Front-face values: 0=CounterClockwise (default),
1=Clockwise. The sign of the integer signed-area test
(EdgeFn(v0, v1, v2)) decides screen-space orientation;
triangles whose orientation matches the cull selection are
dropped before the bbox walk. Default is "no culling".
Out of scope — deferred:
- Texture sampling. The descriptor surface accepts
CombinedImageSamplerbinds but the rasterizer has no per-pixel sampler fetch path; the bound image-view is recorded for stats only. - Perspective-correct attribute interpolation. The shader-path
rasterizer enables it automatically when every triangle vertex
reports a positive
gl_Position.wfrom the VS — each vertex's1/wis computed once, varyings are pre-divided byw, bothvalue/wand1/ware interpolated linearly across the triangle, and a per-pixel divide recovers the correct attribute. Orthographic projections (w == 0or NaN) fall back to the affine path without producing artefacts. The fixed-function v0/v1 vertex format rasterizer remains affine — its inputs are pixel-space pre-projected, so perspective correction would be a no-op there anyway. - Multi-binding vertex buffers — the fixed-function (DuetOS v0/v1
vertex format) rasterizer reads only binding 0. The SPIR-V
shader rasterizer honours per-attribute binding indices via
VkVertexInputAttributeDescriptionwhen an explicit input description is attached (VkSetVertexInputDuet), looking each attribute up against the matchingRasterState::vb_per_bindingslot. - Multi-rect scissor — only the first scissor rect is recorded.
The reason Copy* works while Draw* doesn't: copy operations don't
need shader execution. The framebuffer's pixels move because the
copy is memcpy on CPU memory the framebuffer already mirrors.
The window-system integration is where the ICD meets the compositor:
vkCreateXcbSurfaceKHR/ equivalent — the loader maps the Win32HWND(or native window handle) to aVkSurfaceKHR.vkCreateSwapchainKHR— allocatesNscanout-backed images. Each image's backing memory aliases a slice of the framebuffer that the compositor will present.vkAcquireNextImageKHR— returns the next image index.- The app records draws into a command buffer targeting that image;
submits via
vkQueueSubmit.vkCmdClearColorImageandvkCmdDraw/vkCmdDrawIndexed(CPU edge-function rasterizer with Gouraud interpolation, scissor, software 16-bit depth) both produce real pixels into the scanout image. vkQueuePresentKHR— flushes the damage rect through the compositor.
vkAcquireNextImageKHR does not currently block — there's only one
"frame in flight" so the next image is always available. When real
GPU submission lands, the wait + semaphore mechanics will need to
fill in.
The subsystem keeps a GraphicsStats struct with per-handle "live"
counts plus a few interesting totals:
vk_instance_live,vk_device_live,vk_command_buffer_live, …vk_command_recorded— total opcodes appended across all buffersvk_command_replayed— total opcodes consumed byvkQueueSubmitvk_clear_pixels_painted— sum of pixels written viaClearvk_spirv_entry_points_seen— count of SPIR-V entry points the shader-module parser found (parser runs atvkCreateShaderModule)
The gfxdemo kernel app reads these counters live to render its
"current GPU activity" panel.
graphics_vk_selftest.cpp runs at boot (under
DUETOS_BOOT_SELFTESTS):
- Drives the canonical lifecycle: instance → device → queue → pipeline → descriptor → memory/buffer/image → command tape → submit → teardown, asserting every live handle pool returns to zero.
- Deliberately does NOT clear a scanout-backed image — the boot console owns the framebuffer at this point.
- Image-backed paint leg: a non-scanout BGRA8
VkImagebound to host-visible memory takes a tape-recorded clear + one v0-format triangle draw; the test asserts the clear color at the corners, the triangle color at an interior pixel, and thekVkStatsImageClearPixelscounter delta, then emits the grep-able[vk-selftest] PASS (image-backed clear+draw)line the boot smoke checks. This is the kernel half of the D3D11→Vulkan back-buffer path, proven on every QEMU boot.
A failure leaves a [selftest:graphics] WARN sentinel and fails
the boot self-test gate — graphics is foundational enough that
booting through a broken ICD will produce nonsense for every
consumer downstream.
D3D11 (userland/libs/d3d11/) routes its
swap chain through this ICD (v0, since the D3D11→Vulkan thunk slice):
D3D11CreateDeviceAndSwapChainunder driver type UNKNOWN / HARDWARE / REFERENCE builds theSYS_VK_CALLladder (userland/libs/dx_vk.h): instance → physical device → device → queue → host-visible memory → non-scanout BGRA8VkImage(bound + mapped) → command pool/buffer → a 64 KiB mapped vertex staging buffer.ClearRenderTargetViewrecordsCmdClearColorImageand triangleDraw*calls recordCmdBindVertexBuffer+CmdDrawover DuetOS v0 vertex records into the kernel command tape; the userland software rasterizer does not run on this path.Present(or a CPU readbackMap) ends + submits the tape;vkQueueSubmitreplays it and the KERNEL rasterizer paints the image backing, which the DLL syncs into the user-heap back buffer for the existingSYS_GDI_BITBLTpresent.
WARP / SOFTWARE / NULL driver types — and any vk-setup failure —
stay on the userland software rasterizer (dx_raster.h). D3D12
(userland/libs/d3d12/) has not been
moved onto this path yet; it still paints in userland.
- One global spinlock protects the per-kind handle pools — handle allocation is rare, so the contention cost is negligible.
- Command-buffer recording is per-buffer; concurrent recording of different buffers is lock-free.
vkQueueSubmittakes the queue's spinlock and serialises submits on that queue.- Boot self-test runs single-threaded.
Graphics runs in kernel context; the ICD doesn't gate at the Vulkan-call level. Capability checks happen one layer up — at the syscalls that the Win32 thunks issue to reach the ICD. See Capabilities.
graphics_vk_spirv_*.{h,cpp} is a freestanding SPIR-V parser +
executor inside the kernel. At vkCreateShaderModule time the
module is parsed into a Program structure (types, constants,
variables, basic blocks, instructions) and stored alongside the
existing ShaderModuleInfo. The interpreter runs the program on
demand — once per vertex (vertex shader) or once per pixel
(fragment shader) — when the shader-rasterizer hook drives a
vkCmdDraw against a pipeline that has parseable VS + FS
modules bound.
Untrusted-input hardening (2026-06-17): the parser stores SPIR-V
result ids and operand ids raw (no < kMaxIds clamp), so every executor
function that indexes the fixed [kMaxIds] ExecContext / Program
tables bounds the id at its choke point — SetScalar, AllocComposite,
IsComposite, DoLoad, DoStore, DoAccessChain all reject
id == 0 || id >= kMaxIds before touching the table. Without this a
module with result_id ≥ 512 was a guest-driven OOB write into kernel
.bss. See Design-Decisions
(2026-06-17).
Float math goes through util/soft_float.{h,cpp} —
an IEEE 754 binary32 implementation in pure integer code, because
the kernel is compiled -mno-sse -mno-sse2 and cannot link any
compiler-rt soft-float helpers. Sf32 covers Add / Sub / Mul / Div
/ Sqrt / Neg / Abs / Min / Max / Clamp / Mix / Step + comparison
- int<->float conversion, all with NaN-unordered IEEE semantics. 43-vector boot self-test validates the implementation.
Opcodes the executor dispatches today:
| Family | Opcodes |
|---|---|
| Memory | OpLoad, OpStore, OpAccessChain |
| Composite | OpVectorShuffle, OpCompositeConstruct, OpCompositeExtract |
| Arithmetic (int) | OpSNegate, OpIAdd, OpISub, OpIMul, OpSDiv, OpUDiv |
| Arithmetic (float) | OpFNegate, OpFAdd, OpFSub, OpFMul, OpFDiv |
| Vector ops | OpVectorTimesScalar, OpMatrixTimesVector, OpDot |
| Conversion | OpConvertSToF, OpConvertUToF, OpConvertFToS, OpBitcast |
| Comparison | OpIEqual, OpINotEqual, OpSLessThan, OpFOrdLessThan |
| Control flow | OpBranch, OpBranchConditional, OpPhi, OpReturn, OpReturnValue, OpLoopMerge, OpSelectionMerge |
| Function call | OpFunctionCall — real call: positional arg→OpFunctionParameter binding, callee block execution, OpReturnValue→result copy |
| Extended | OpExtInst against GLSL.std.450: Sqrt, Sin, Cos, Pow, FMin, FMax, FClamp, FMix, Step, Length, Normalize, Cross |
Per-shader step budget (kStepBudget = 8192) caps runaway loops.
Function calls are real, not inlined-away by assumption: a
front-end (DXC especially) that leaves helper functions as
OpFunctionCall now executes correctly. Each argument is bound to the
matching OpFunctionParameter (value params alias the argument's SSA
value; pointer/inout params bind the storage-class-packed pointer),
the callee's basic blocks run on the shared ExecContext (SPIR-V ids
are module-unique, so no per-frame SSA save/restore is needed beyond
the caller's control-flow cursor), and the callee's OpReturnValue is
copied into the call result. Vulkan forbids shader recursion, but the
module is untrusted — a crafted self-/mutually-recursive function
would otherwise recurse ExecuteCallee→ExecuteBlock→ExecuteCallee until
the kernel stack overflows (the per-instruction kStepBudget bounds
work, not native depth). ExecContext::call_depth caps recursion at
kMaxCallDepth = 16; over-depth is an immediate void return. GAP: an
OpAccessChain whose base is itself a pointer parameter (chained
access through an inout struct param) still needs a Variable base —
rare outside hand-written GLSL. Boot self-test module 6
(add(a,b) helper called from main) pins the path.
Transcendental support: Sf32Sin / Sf32Cos use a 7th-order
minimax polynomial after [-pi/2, pi/2] range reduction (~3e-5 max
error on the reduced range). Sf32Exp / Sf32Log use degree-5
polynomial expansions with 2^n / log2 mantissa decomposition; Sf32Pow
composes exp(y * log(x)) for positive bases. The soft-float
self-test asserts each with ULP-tolerance bounds — Sf32SelfTest
emits [util/soft_float] self-test PASS (55 vectors) on clean
boot.
Shader-rasterizer hook (graphics_vk_shaderraster.cpp):
- When
vkCmdDrawreplays and a graphics pipeline with VS + FS is bound,ShaderRasterizeDrawruns the SPIR-V VS once per vertex to computegl_Position, then for each pixel inside the resulting triangle runs the FS to compute the colour. - Vertex input layout: caller-supplied
VkVertexInputAttributeDescriptionviaVkSetVertexInputDuet— each VS Input is fetched at the declared (binding, offset) tuple with the right stride. Falls back to a canonical 16-byte-per-Location layout when no description is attached. - Fragment output: Location 0 vec4 RGBA, clamped to [0,1] and packed BGRA8 for the framebuffer.
- Topology: TriangleList only on the shader path; other topologies fall back to the fixed-function rasterizer.
- Painted-pixel cap (65k per draw) so a runaway fullscreen shader cannot brick the boot.
Compute shader dispatch (ShaderDispatchCompute):
vkCmdDispatchreplay now routes through the SPIR-V interpreter for compute pipelines. The parser capturesOpExecutionMode LocalSize x y z; the dispatcher runs the entry pointgroup_count_xyz * local_size_xyztimes.- Per-invocation builtins set before each execution:
gl_NumWorkgroups,gl_WorkgroupId,gl_LocalInvocationId,gl_GlobalInvocationId,gl_LocalInvocationIndex. - 65k-invocation cap per dispatch protects against pathological
dispatch(1024,1024,1024)runs.
Per-pixel varying interpolation (v2):
- After the VS runs per vertex,
RunVertexShadersnapshots every Location-decorated Output variable into aVaryingSnapshot[8]array (up to 8 varyings, each up to 16 Sf32 components). PaintTriangleprecomputes1 / |area2|as an Sf32 once per triangle; per pixel it derives barycentric weights as|edge_i| * inv_area(three Sf32 multiplies, no divides).- For each varying, the per-pixel value =
a*w0 + b*w1 + c*w2viaBaryLerp(three multiplies + two adds per component). - The interpolated values are written to the matching FS Input
Location via
spirv::WriteInputLocationbefore invoking the FS. Result: a fragment shader that readsin vec3 colorfrom a Location varying sees a smoothly interpolated value, not zero.
Perspective-correct interpolation (v3):
- When
gl_Position.w > 0for every triangle vertex,ShaderRasterizeDrawcomputes1/wper vertex and passes the triple intoPaintTriangle. - Per vertex, each varying value is pre-divided by
w. Per pixel, bothvalue/wand1/ware barycentric-interpolated linearly, then divided to recover the perspective-correct varying. - Degenerate / orthographic projections (
w <= 0or NaN) fall back to the affine path — no artefacts on the cases where perspective correction is meaningless.
Texture sampling primitives (v3):
- The parser recognises
OpTypeImage/OpTypeSampler/OpTypeSampledImageand decorates variables withDescriptorSet/Bindingindices. OpSampledImage(combined image+sampler),OpImageSampleImplicitLod, andOpImageSampleExplicitLodexecute. Today they return the UV coordinate as(u, v, 0, 1)— the "missing texture" diagnostic pattern. A shader that samples a 2D texture now produces a smooth gradient instead of zero; the descriptor-set fetch path that replaces the fallback with real texel data lands in the next slice.
Multi-format image support:
VkGetPhysicalDeviceFormatProperties/ImageFormatPropertiesnow recognise six DuetOS-internal format ids:0 = B8G8R8A8_UNORM,1 = R8G8B8A8_UNORM,2 = R8_UNORM,3 = R8G8_UNORM,4 = R16_UNORM,5 = R32G32B32A32_SFLOAT. All report the baseline feature set (sampled / color attachment / transfer); format-aware sample + blit paths land with the texel-fetch slice.
Boot self-tests:
[util/soft_float] self-test PASS (43 vectors)— soft-float primitives.[subsys/graphics/spirv] self-test PASS (3 modules executed)— three hand-crafted SPIR-V modules (constant vec4 fragment, Sf32 add via OpFAdd, vec3*scalar via OpVectorTimesScalar) parse and execute end-to-end with the expected outputs.
The shader hook is OPT-IN — it returns false (no paint) when:
- No pipeline is bound, or
- The pipeline doesn't carry interpretable VS + FS Programs, or
- The topology is anything other than TriangleList.
In all "false" cases the existing fixed-function v0/v1 rasterizer runs, so the existing demos / DirectX clears / boot self-test see no behavioural change.
The in-kernel ICD is reachable from Win32 PE binaries via
vulkan-1.dll, a freestanding userland PE library at
userland/libs/vulkan_1/vulkan_1.c.
The DLL exports the canonical Vulkan entry-point set as thin
thunks over SYS_VK_CALL (syscall 211), an op-code-dispatched
syscall whose rdi argument selects which VkOp to invoke.
| Userland entry | Kernel side (SYS_VK_CALL op) |
|---|---|
vkCreateInstance |
kVkOpCreateInstance |
vkDestroyInstance |
kVkOpDestroyInstance |
vkEnumeratePhysicalDevices |
kVkOpEnumeratePhysicalDevices |
vkCreateDevice |
kVkOpCreateDevice |
vkDestroyDevice |
kVkOpDestroyDevice |
vkGetDeviceQueue |
kVkOpGetDeviceQueue |
vkDeviceWaitIdle |
kVkOpDeviceWaitIdle |
vkQueueWaitIdle |
kVkOpQueueWaitIdle |
vkEnumerateInstanceVersion |
kVkOpGetInstanceVersion |
vkGetInstanceProcAddr |
string -> function-pointer table |
vkGetDeviceProcAddr |
same table |
DuetOS_Vk_GetStatsCounter |
kVkOpGetStatsCounter (diagnostic) |
DuetOS_Vk_ClearFramebufferRgba |
kVkOpClearFramebufferRgba (end-to-end clear-the-screen — same path vkCmdClearColorImage takes for a scanout image) |
DuetOS_Vk_CreateSurface |
kVkOpCreateSurfaceDuet (the kernel's single platform-agnostic VkSurfaceKHR bound to the framebuffer) |
DuetOS_Vk_DestroySurface |
kVkOpDestroySurface |
DuetOS_Vk_Present |
kVkOpPresent (flushes framebuffer through the compositor present hook — equivalent to vkQueuePresentKHR on a single-image swapchain) |
vkCreateShaderModule / vkDestroyShaderModule |
kVkOpCreateShaderModule / kVkOpDestroyShaderModule (kernel copies the SPIR-V word stream + invokes the v1 parser; module survives until destroyed) |
vkAllocateMemory / vkFreeMemory |
kVkOpAllocateMemory / kVkOpFreeMemory (host-visible coherent memory type) |
vkCreateBuffer / vkDestroyBuffer |
kVkOpCreateBuffer / kVkOpDestroyBuffer |
vkBindBufferMemory |
kVkOpBindBufferMemory |
vkMapMemory / vkUnmapMemory |
kVkOpMapMemory / kVkOpUnmapMemory (returns the kernel's kheap-backed host pointer; userland reads / writes directly since v0 has no per-process VM gate on this surface) |
vkCreateImage / vkDestroyImage / vkBindImageMemory |
kVkOpCreateImage / kVkOpDestroyImage / kVkOpBindImageMemory (BGRA8 default format) |
vkCreateCommandPool / vkDestroyCommandPool |
kVkOpCreateCommandPool / kVkOpDestroyCommandPool |
vkAllocateCommandBuffers |
kVkOpAllocateCommandBuffer (v0 single buffer per call) |
vkBeginCommandBuffer / vkEndCommandBuffer |
kVkOpBeginCommandBuffer / kVkOpEndCommandBuffer |
vkCmdClearColorImage |
kVkOpCmdClearColorImage (packs the float-4 color into the canonical 0xAARRGGBB word; userland-side _fltused handles the float compare) |
vkQueueSubmit |
kVkOpQueueSubmit (v0 single command buffer per submit) |
vkCreatePipelineLayout / vkDestroyPipelineLayout |
kVkOpCreatePipelineLayout / kVkOpDestroyPipelineLayout |
vkCreateRenderPass / vkDestroyRenderPass |
kVkOpCreateRenderPass / kVkOpDestroyRenderPass |
vkCreateGraphicsPipelines |
kVkOpCreateGraphicsPipeline (extracts VS / FS shader modules from pStages by hand) |
vkCreateComputePipelines |
kVkOpCreateComputePipeline |
vkDestroyPipeline |
kVkOpDestroyPipeline |
vkCmdBindPipeline / vkCmdDraw / vkCmdDispatch |
kVkOpCmdBindPipeline / kVkOpCmdDraw / kVkOpCmdDispatch |
vkCmdBindVertexBuffers / vkCmdBindIndexBuffer |
kVkOpCmdBindVertexBuffer / kVkOpCmdBindIndexBuffer (v0 single-binding form) |
vkUpdateDescriptorSets |
kVkOpUpdateDescriptorSet (walks the writeCount array, one syscall per entry; extracts the image-view handle from each VkDescriptorImageInfo) |
vkCreateDescriptorSetLayout / vkDestroyDescriptorSetLayout |
kVkOpCreateDescriptorSetLayout / kVkOpDestroyDescriptorSetLayout (single binding-0 CombinedImageSampler in v0) |
vkCreateDescriptorPool / vkDestroyDescriptorPool |
kVkOpCreateDescriptorPool / kVkOpDestroyDescriptorPool |
vkAllocateDescriptorSets |
kVkOpAllocateDescriptorSet (v0 single set per call) |
vkCmdBindDescriptorSets |
kVkOpCmdBindDescriptorSet (v0 single set per bind) |
SYS_VK_CALL plus VkOp / VkStatsCounter enums are in
kernel/syscall/syscall.h;
the dispatch lives in
kernel/syscall/syscall_vk.cpp.
One syscall + an op-code-dispatch keeps the syscall number space
sane while preserving a stable per-op ABI value — once published,
neither the syscall number nor the op-code may move.
What a Vulkan-using Win32 PE can do today: load the DLL, resolve
exports, walk the full lifecycle, create + bind + map host-visible
memory / buffers / images, create shader modules (the kernel
copies the SPIR-V stream in), record + submit command buffers, and
read the diagnostic stats counters. The D3D11 DLL exercises this
surface end-to-end as its Vulkan back end (see "DirectX
Translation Hand-off" above) — it issues the ops directly from
userland/libs/dx_vk.h rather than importing vulkan-1.dll,
because the DX DLLs are freestanding single-TU builds.
SYS_VK_CALL is reachable from any Win32 PE, so every op treats its
trap-frame arguments as hostile. Two disciplines are enforced uniformly:
- No raw guest-pointer dereference in the ICD. Scalar out-params go
through
mm::CopyToUser/CopyFromUser(UserStore/UserLoad), andOpCreateShaderModulebounces the SPIR-V into a capped (16 MiB) kernel buffer viaCopyFromUserbefore the parser ever reads it — the ICD keeps its own owning copy, so the bounce is freed immediately. A bad / unmapped / read-only guest pointer or an oversized length yields a clean failure, not a kernel#PFor a multi-GBKMalloc. - Every
Bind*Memoryvalidates the region fits the memory object. BothVkBindBufferMemoryandVkBindImageMemoryrequireoffset <= mem.size && required <= mem.size - offset, whererequireduses the format-correctBytesPerTexelForFormatstride (also reported byVkGetImageMemoryRequirements).VkAllocateMemorycaps the guest size. Without the image-side check a guest could bind a large image into a small allocation and OOB-write the kernel heap through a SPIR-VOpImageWrite.
The signal test ("could a malicious PE do something a native process couldn't?") must stay NO for this surface — new ops added to the table above inherit the same obligation.
Two ops worth calling out: kVkOpCreateImage masks the
scanout-backed flag from the userland path (a PE must not be able
to mint an image whose clears paint the live framebuffer), and
kVkOpFreeCommandBuffer (52) releases the cb slot
kVkOpAllocateCommandBuffer took — destroy-pool alone does not.
- No real GPU submission. Every device-side command is replayed
on the CPU; the visible effects are
Clear, resource copies, the CPU triangle rasterizer forvkCmdDraw/vkCmdDrawIndexed(TriangleList / TriangleStrip / TriangleFan; Gouraud-shaded with per-pixel src-over alpha; software Z-test when v1 vertex format is selected), AND the SPIR-V shader rasterizer when a pipeline binds parseable VS + FS modules (TriangleList only). - SPIR-V texture sampling.
OpImageSampleImplicitLod/OpImageSampleExplicitLodfetch through the bound (set 0, binding 0) sampled-image descriptor viaSampleImageRgba8. The per-axis addressing mode is driven by the VkSampler the caller pinned at descriptor-update time —VkCreateSamplerrecordsaddressModeU/_V/_Winto a per-handleSamplerRecord,VkUpdateDescriptorSetSampledpropagates the VkSampler handle alongside the VkImageView, and the executor reads bothSamplerAddressModeFor(handle)(U) andSamplerAddressModeVFor(V) on every sample so an asymmetric sampler like(REPEAT_U, CLAMP_TO_EDGE_V)produces tileable-X clamped-Y output. ThemagFilterpropagates too —SamplerMagFilterForpicks 0=Nearest / 1=Linear; the Nearest path short-circuits the bilerp to a single-texel fetch (the previous always-bilinear behaviour silently degraded VK_FILTER_NEAREST samples into blurred ones). ClampToBorder is per-axis too — a UV component outside [0, 1] on a border-mode axis returns the border colour, while the other axis follows its own fold. v0's CLAMP_TO_BORDER border colour is always transparent black (0,0,0,0); per-sampler border tints land whenVkSamplerCreateInfogrows aborderColorfield. Unbound samples still return the UV coordinate as(u, v, 0, 1)— the "missing texture" diagnostic. Explicit LOD (no mipmap chain) is still unimplemented; the W axis is recorded but only meaningful for 3D / cube images that v0 doesn't yet ship. - SPIR-V storage-image access.
OpImageRead/OpImageFetch(integer-coord unfiltered read) andOpImageWrite(integer-coord unfiltered store) execute against the bound (set 0, binding 0) image, dispatched by the image's recorded format. All six DuetOS-internal formats round-trip correctly: BGRA8_UNORM, R8G8B8A8_UNORM (1 byte/channel × 4); R8_UNORM (1 B/texel); R8G8_UNORM (2 B/texel); R16_UNORM (2 B/texel, 16-bit precision); and R32G32B32A32_SFLOAT (16 B/texel, raw f32 — HDR-precision preserved across the round-trip).VkCreateImageWithFormattakes a DuetOS format id (0–5) and records it inImageRecord; legacyVkCreateImagedefers with format=0 so existing callers see no change. Out-of-bounds reads return(0, 0, 0, 1)and out-of-bounds writes are silently dropped per spec. Coordinate input is signed integer scalar or vector; the first two components are consumed (2D). Used by compute shaders that walk a storage image — boot self-test pins both the BGRA8 and the R8_UNORM + R32G32B32A32_SFLOAT round-trips along with the OOB-no-clobber invariant. Explicit LOD (no mipmap chain) and multisample images remain unimplemented. - SPIR-V derivative / barrier / atomic opcodes.
OpDPdx,OpDPdy,OpFwidth(plusFine/Coarsevariants) return zero — GAP: real derivatives need 2×2-quad fragment execution that the serial interpreter doesn't model.OpControlBarrierandOpMemoryBarrierare no-ops because invocations run serially (the barrier is satisfied trivially).OpAtomicLoad,OpAtomicStore, and the RMW family (Exchange/IIncrement/IDecrement/IAdd/ISub/SMin/UMin/SMax/UMax/And/Or/Xor) collapse to non-atomic equivalents — correct on serial execution, will need real atomicity once compute dispatch runs invocations in parallel.OpFRem/OpFModdispatch through the soft-float scalar path (FRem = trunc-quotient remainder, FMod = floor-quotient with sign of divisor). - SPIR-V perspective correction. The shader rasterizer is affine (linear pixel-space interpolation in pixel space). Perspective-correct attribute interpolation needs a per-fragment 1/w divide which the v2 hook doesn't perform.
- Vertex input descriptions. The shader hook uses a canonical
16-byte-per-Location layout instead of consuming the caller's
VkVertexInputAttributeDescription/VkVertexInputBindingDescription. A caller whose vertex layout differs gets garbage values fed into the VS Input variables. - Render targets.
vkCmdDraw/vkCmdDrawIndexedand clear replay paint scanout-backed images (the framebuffer) and image-backed targets (non-scanout images bound to host-visible memory) — but image-backed paint is BGRA8-only and the software depth test is scanout-only (the shared depth surface is sized to the framebuffer extent). vulkan-1.dllregister misalignment. Severalvulkan_1.ccall sites pass their first payload argument inrsi, which no kernelSYS_VK_CALLhandler reads (the kernel convention is rdx/r10/r8/r9) — e.g.vkBeginCommandBuffer,vkCreateDevice,vkEnumeratePhysicalDevices,vkCmdClearColorImage,DuetOS_Vk_GetStatsCounter. Those entries are latently broken for any PE that calls them;userland/libs/dx_vk.hbinds to the kernel convention and is unaffected. Fixing vulkan_1.c's argument placement is a follow-on slice.- Per-process cleanup. Kernel handle pools are global with a fixed capacity and no per-process ledger — a PE that exits without destroying its Vulkan handles leaks the slots until reboot. The D3D11 backend tears down on swap-chain destroy; a killed PE doesn't get the chance.
- Single queue family. No async compute, no transfer queue separation.
- No swapchain resize. Recreating the swapchain is supported; resizing the underlying framebuffer is not.
- No multi-monitor. Single scanout.
- D3D9 / D3D12 / DirectDraw don't route through the Vulkan path yet — they paint their back buffers with the userland software rasterizer and BitBlt. D3D11 is the first DX front end on the Vulkan back end (see "DirectX Translation Hand-off").
- DirectX — D3D translation that builds on this ICD
- Graphics Drivers — per-vendor GPU drivers that will eventually consume real Vulkan submits
- Compositor and Window Manager — what consumes
vkQueuePresentKHR - Subsystem Isolation — why the ICD is in the kernel
- Win32 DLLs —
vulkan-1.dll,dxgi.dll - Win32 Surface Status — per-export REAL / STUB / MISSING inventory
- GPU Implementation Notes — cross-vendor prior-art for the per-vendor submission path, SPIR-V sampler / texel-fetch math, and the DXBC→SPIR-V plan