Favour compile-time dispatch

This commit is contained in:
2026-07-05 15:05:50 +01:00
parent 0f00ea9586
commit 1e0c195f42
@@ -359,10 +359,11 @@ void fs_main(...) { ... }
``` ```
src/prism/ src/prism/
├── rhi/ ├── rhi/
│ ├── pr_rhi.h ← Public API (backend-agnostic) │ ├── pr_rhi.h ← Umbrella header: canonical API + dispatch
│ ├── pr_rhi_types.h ← Shared types (PrRhiBufferDesc, etc.) │ ├── pr_rhi_types.h ← Shared types (PrRhiBufferDesc, etc.)
│ ├── vulkan/ │ ├── 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_device.c
│ │ ├── pr_rhi_vk_buffer.c │ │ ├── pr_rhi_vk_buffer.c
│ │ ├── pr_rhi_vk_texture.c │ │ ├── pr_rhi_vk_texture.c
@@ -371,7 +372,8 @@ src/prism/
│ │ ├── pr_rhi_vk_descriptor.c │ │ ├── pr_rhi_vk_descriptor.c
│ │ ├── pr_rhi_vk_command.c │ │ ├── pr_rhi_vk_command.c
│ │ └── pr_rhi_vk_swapchain.c │ │ └── pr_rhi_vk_swapchain.c
── d3d12/ ← (future) ── d3d12/ ← (future)
│ └── metal/ ← (future)
└── ... └── ...
``` ```
@@ -390,27 +392,148 @@ void prRhiCmdCopyBuffer(PrRhiCommandBuffer *cb,
PrRhiBuffer *src, PrRhiBuffer *dst); 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 ```c
// pr_rhi.h // pr_rhi.h
typedef struct PrRhiDevice PrRhiDevice; #ifndef PR_RHI_H
struct PrRhiDevice { #define PR_RHI_H
PrRhiDeviceVtbl *vtbl; // function pointer table
void *backend; // VkDevice or ID3D12Device
};
// Each backend fills the vtbl #include "pr_rhi_types.h"
typedef struct PrRhiDeviceVtbl {
PrRhiBuffer* (*createBuffer)(PrRhiDevice*, const PrRhiBufferDesc*, WpAllocator*); // ── Public API (documented here) ────────────────────────────────────
void (*destroyBuffer)(PrRhiBuffer*, WpAllocator*); PrRhiDevice *prRhiCreateDevice(const PrRhiDeviceDesc *desc, WpAllocator *alloc);
// ... etc void prRhiDestroyDevice(PrRhiDevice *device, WpAllocator *alloc);
} PrRhiDeviceVtbl; 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 Each aliases header maps the generic names to the backend's concrete names:
the API clean. The renderer calls `device->vtbl->createBuffer(...)` and the
backend resolves the call. ```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 ### 8.4 Frame lifecycle