From 1e0c195f42fba04abacb1981d4ca2ead537e6424 Mon Sep 17 00:00:00 2001 From: Abdelrahman Date: Sun, 5 Jul 2026 15:05:50 +0100 Subject: [PATCH] Favour compile-time dispatch --- .../research/rendering-hardware-interface.md | 159 ++++++++++++++++-- 1 file changed, 141 insertions(+), 18 deletions(-) diff --git a/documents/research/rendering-hardware-interface.md b/documents/research/rendering-hardware-interface.md index 698b5c1..5dddefa 100644 --- a/documents/research/rendering-hardware-interface.md +++ b/documents/research/rendering-hardware-interface.md @@ -359,10 +359,11 @@ void fs_main(...) { ... } ``` src/prism/ ├── rhi/ -│ ├── pr_rhi.h ← Public API (backend-agnostic) +│ ├── pr_rhi.h ← Umbrella header: canonical API + dispatch │ ├── pr_rhi_types.h ← Shared types (PrRhiBufferDesc, etc.) │ ├── vulkan/ -│ │ ├── pr_rhi_vk.h ← Vulkan backend internal header +│ │ ├── pr_rhi_vk.h ← Declares prRhiCreateDeviceVk, etc. +│ │ ├── pr_rhi_vk_aliases.h ← #define prRhiCreateDevice prRhiCreateDeviceVk │ │ ├── pr_rhi_vk_device.c │ │ ├── pr_rhi_vk_buffer.c │ │ ├── pr_rhi_vk_texture.c @@ -371,7 +372,8 @@ src/prism/ │ │ ├── pr_rhi_vk_descriptor.c │ │ ├── pr_rhi_vk_command.c │ │ └── pr_rhi_vk_swapchain.c -│ └── d3d12/ ← (future) +│ ├── d3d12/ ← (future) +│ └── metal/ ← (future) └── ... ``` @@ -390,27 +392,148 @@ void prRhiCmdCopyBuffer(PrRhiCommandBuffer *cb, PrRhiBuffer *src, PrRhiBuffer *dst); ``` -### 8.3 Backend dispatch (compile-time) +### 8.3 Backend dispatch (compile-time via preprocessor aliases) + +Backend selection happens at compile time via preprocessor aliases — no vtbl, +no runtime dispatch overhead. Each backend is a set of standalone `.c` files; +the build system compiles only the selected backend's sources. + +``` +src/prism/rhi/ +├── pr_rhi.h ← umbrella: canonical API + dispatch +├── pr_rhi_types.h ← shared types (all backends include this) +├── vulkan/ +│ ├── pr_rhi_vk.h ← declares prRhiCreateDeviceVk, etc. +│ ├── pr_rhi_vk_aliases.h ← #define prRhiCreateDevice prRhiCreateDeviceVk +│ ├── pr_rhi_vk_device.c +│ └── pr_rhi_vk_buffer.c +├── d3d12/ +│ ├── pr_rhi_d3d12.h ← declares prRhiCreateDeviceD3D12, etc. +│ ├── pr_rhi_d3d12_aliases.h ← #define prRhiCreateDevice prRhiCreateDeviceD3D12 +│ └── pr_rhi_d3d12_device.c +└── metal/ + ├── pr_rhi_metal.h + ├── pr_rhi_metal_aliases.h + └── pr_rhi_metal_device.c +``` + +The umbrella header documents the public API and conditionally includes the +selected backend's aliases: ```c // pr_rhi.h -typedef struct PrRhiDevice PrRhiDevice; -struct PrRhiDevice { - PrRhiDeviceVtbl *vtbl; // function pointer table - void *backend; // VkDevice or ID3D12Device -}; +#ifndef PR_RHI_H +#define PR_RHI_H -// Each backend fills the vtbl -typedef struct PrRhiDeviceVtbl { - PrRhiBuffer* (*createBuffer)(PrRhiDevice*, const PrRhiBufferDesc*, WpAllocator*); - void (*destroyBuffer)(PrRhiBuffer*, WpAllocator*); - // ... etc -} PrRhiDeviceVtbl; +#include "pr_rhi_types.h" + +// ── Public API (documented here) ──────────────────────────────────── +PrRhiDevice *prRhiCreateDevice(const PrRhiDeviceDesc *desc, WpAllocator *alloc); +void prRhiDestroyDevice(PrRhiDevice *device, WpAllocator *alloc); +PrRhiBuffer *prRhiCreateBuffer(PrRhiDevice *d, const PrRhiBufferDesc *desc, WpAllocator *a); +void prRhiDestroyBuffer(PrRhiBuffer *b, WpAllocator *a); +// ... etc + +// ── Backend dispatch ────────────────────────────────────────────── +#if defined(PR_RHI_VULKAN) +# include "vulkan/pr_rhi_vk_aliases.h" +#elif defined(PR_RHI_D3D12) +# include "d3d12/pr_rhi_d3d12_aliases.h" +#elif defined(PR_RHI_METAL) +# include "metal/pr_rhi_metal_aliases.h" +#else +# error "Define one of: PR_RHI_VULKAN, PR_RHI_D3D12, PR_RHI_METAL" +#endif + +#endif ``` -This gives zero-cost abstraction (pointer indirection on calls) while keeping -the API clean. The renderer calls `device->vtbl->createBuffer(...)` and the -backend resolves the call. +Each aliases header maps the generic names to the backend's concrete names: + +```c +// vulkan/pr_rhi_vk_aliases.h +#ifndef PR_RHI_VK_ALIASES_H +#define PR_RHI_VK_ALIASES_H + +#include "pr_rhi_vk.h" + +#define prRhiCreateDevice prRhiCreateDeviceVk +#define prRhiDestroyDevice prRhiDestroyDeviceVk +#define prRhiCreateBuffer prRhiCreateBufferVk +#define prRhiDestroyBuffer prRhiDestroyBufferVk + +#endif +``` + +The backend implementation headers declare only their own real names: + +```c +// vulkan/pr_rhi_vk.h +#ifndef PR_RHI_VK_H +#define PR_RHI_VK_H + +#include "../pr_rhi_types.h" + +PrRhiDevice *prRhiCreateDeviceVk(const PrRhiDeviceDesc *desc, WpAllocator *alloc); +void prRhiDestroyDeviceVk(PrRhiDevice *device, WpAllocator *alloc); +// ... + +#endif +``` + +Backend `.c` files are normal standalone translation units — no `.c` inclusion: + +```c +// vulkan/pr_rhi_vk_device.c +#include "pr_rhi_vk.h" + +PrRhiDevice *prRhiCreateDeviceVk(const PrRhiDeviceDesc *desc, WpAllocator *alloc) { + // ... +} +``` + +Render code uses the generic names via the umbrella: + +```c +#include "prism/rhi/pr_rhi.h" + +int main(void) { + PrRhiDevice *dev = prRhiCreateDevice(&desc, &scratch); // → prRhiCreateDeviceVk + // ... +} +``` + +Or explicitly selects a backend by including its header directly: + +```c +#include "prism/rhi/vulkan/pr_rhi_vk.h" + +int main(void) { + PrRhiDevice *dev = prRhiCreateDeviceVk(&desc, &scratch); // real name, no alias + // ... +} +``` + +The build system controls selection by defining the preprocessor macro and +listing only the chosen backend's `.c` files: + +```sh +# Vulkan build +clang -DPR_RHI_VULKAN \ + main.c \ + src/prism/rhi/vulkan/pr_rhi_vk_device.c \ + src/prism/rhi/vulkan/pr_rhi_vk_buffer.c \ + src/wapp/wapp.c \ + -o compositor +``` + +**Properties:** +- Zero runtime overhead (macro expansion is a text substitution) +- Dead code elimination is automatic — unselected backends are never compiled +- Transparent debugging — stack traces show `prRhiCreateDeviceVk` directly +- Documentation lives in one place — the umbrella `pr_rhi.h` +- Each backend is a proper compilation unit — no `#include` of `.c` files +- Explicit override path for single-backend builds or testing ### 8.4 Frame lifecycle